Release a certain number of chapters daily.

Hello, I have a django project related to an online teaching system. There is a course with several chapters. The user can do, for example, two chapters a day. So I would like him to have access to just those two chapters. If on day X, he only did one of the two, then on the next he would do the pending chapter and another new one, from the other day, always remaining two chapters a day. If he does both on the same day, as he really should, then the system blocks it and he has to wait for the next day to continue the course.

So, basically, the logic would be like this, with, for example, three chapters per day:

I already tried to do this in several ways, using the DB, but I still couldn’t find the best solution. If you need any code, just say so, please.

Thanks for the help.

First, what do you consider a “day”? Are you referring to a calendar day, or any arbitrary 24-hour period starting with the time that the first chapter is started?
If a “calendar day”, are you accounting for the individual’s time zone?

Do you already have code that tracks when a person starts a chapter?

Do you already have code that determines whether or not a person can start a chapter?

Yes, it would be helpful to see your models for the users, the chapters, and whatever models you’re using to relate those two models.

You wrote:

Does this mean that you already have working code, but it doesn’t “feel right” to you? If so, posting that code would help, too.

1 Like

Yes, it’s a “calendar day”. After midnight, it’s considered another day.

I have three methods related to this, all returning an array:
one returns the number of chapters completed, another the number of chapters not completed, and the one that returns the number of total chapters, of course.

No, I don’t. From the moment she has the course, she can already make the chapters, but I would like to limit the number of daily chapters, with a number that I defined myself.

Yeah, sure.

class Course(models.Model):
    name = models.CharField(
        verbose_name="Nome",
        max_length=100,
    )

    slug = models.CharField(
        verbose_name="Slug",
        max_length=100,
    )

    duration = models.IntegerField(
        verbose_name=u'Duração (em horas)',
        blank=True,
        null=True,
    )

    price = CurrencyField(
        verbose_name=u'Preço',
        decimal_places=6,
        max_digits=10,
    )

    short_description = models.TextField(
        verbose_name=u'Breve Descrição',
        blank=True,
    )

    description = PlaceholderField('course_description')

    imagebuy = FilerFileField(
        verbose_name='Imagem descritiva',
        null=True,
        blank=True,
        related_name='courseimage',on_delete=models.CASCADE,
    )

    pdf = FilerFileField(
        null=True,
        blank=True,
        related_name='coursepdf',on_delete=models.CASCADE,
    )

    certificate = FilerFileField(
        null=True,
        blank=True,
        verbose_name='Certificado (Frente)',
        related_name='coursecertificate',on_delete=models.CASCADE,
    )
    
    certificate_text = models.TextField(
        verbose_name='Texto certificado',
        default='''
            Certificamos que **{{ name }}** matrĂ­cula nÂş **{{ acquired.registration }}**,
            portando o **CPF {{ cpf }}** concluiu o curso de **{{ acquired.course.name }}** 
            com carga horária de {{ course.duration }} horas. Iniciado em **{{first_access}}**
            ConcluĂ­do em **{{ conclusion }}**.''',
        blank=True,
    )

    certificate_rear = FilerFileField(
        null=True,
        blank=True,
        verbose_name='Certificado (Verso)',
        related_name='coursecertificaterear',on_delete=models.CASCADE,
    )

    enabled = models.BooleanField(
        verbose_name='Habilitado',
        default=True,
    )

    members = models.ManyToManyField(User, through='Acquired')

    sum_slider = models.BooleanField(
        verbose_name='Contabilizar Slider',
        default=False)

    url_simulador = models.CharField(
        max_length=255,
        null=True,
        blank=True,
        default=""
    )

    sum_simulator = models.BooleanField(
        verbose_name='Contabilizar Simulador',
        default=False
    )

    **chapters_per_day = models.CharField(**
**        choices=(**
**            ('1', '1'), ('2', '2'), ('3', '3'), ('4', '4'), ('5', '5'), ('6', '6'), ('7', '7'),**
**            ('8', '8'), ('9', '9'), ('10', '10'), ('11', '11'), ('12', '12'),**
**        ),**
**        default=2,**
**        max_length=14,**
**        null=True,**
**        verbose_name="NĂşmero de capĂ­tulos por dia"**
**    )**

    saturday_acess_bloqued = models.BooleanField(
        default=False,
        verbose_name='Acesso aos sábados bloqueado'
    )

    sunday_acess_bloqued = models.BooleanField(
        default=False,
        verbose_name='Acesso aos domingos bloqueado'
    )

    message_for_bloqued_weekend = models.TextField(
        blank=True,
        null=True,
        max_length=300,
        verbose_name="Mensagem para aluno quando acesso no fim de semana estiver bloqueado"
    )

    times_for_acess = models.ManyToManyField(TimesForAcess, blank=True, verbose_name="Horário de acesso do curso")

    class Meta:
        verbose_name = "Curso"
        ordering = ['name']

    def __unicode__(self):
        return u'%s%s' % (
            self.name,
            ' (%dh)' % self.duration if self.duration else '',
        )

    def __str__(self):
        return u'%s%s' % (
            self.name,
            ' (%dh)' % self.duration if self.duration else '',
        )

    def payment(self, request):
        qs = Acquired.objects.filter(
            Q(date_expire__isnull=True) | Q(date_expire__gte=timezone.now()),
            course__id=self.id,
            user__id=request.user.id,
            paid=False,
        )
        return qs.exists()

    def valid(self, request):
        qs = Acquired.objects.valid().filter(
            course__id=self.id, user__id=request.user.id
        )
        if qs.exists():
            return qs[0]
        return False


class Chapter(models.Model):
    course = models.ForeignKey(
        Course,
        related_name='chapters',
        verbose_name=u'Curso',on_delete=models.CASCADE,
    )

    name = models.CharField(
        verbose_name=u'CapĂ­tulo',
        max_length=500,
    )

    pdf = FilerFileField(null=True, blank=True,on_delete=models.CASCADE,)

    downloads = FilerFolderField(null=True, blank=True,on_delete=models.CASCADE,)

    order = models.IntegerField(
        verbose_name='Ordem',
        default=0,
    )

    def slider(self):
        try:
            return Slider.objects.get(chapter=self)
        except Exception  as e:
            return None

    class Meta:
        verbose_name = u'CapĂ­tulo'
        ordering = (
            'course__name','order', 'id',
        )

    def __unicode__(self):
        return u'%s - %s' % (self.course, self.name)

    def __str__(self):
        return u'%s - %s' % (self.course, self.name)

    def _get_read_chapters(self, user):
        acq = Acquired.objects.valid().filter(
            user=user,
            course__id=self.course.id,
        ).order_by('-id')
        if not acq.exists():
            return False, False
        return acq[0], list(filter(
            lambda y: bool(y), acq[0].chapters_read.split(',')
        ))

    def read(self, user):
        acq, chapters = self._get_read_chapters(user)
        if not acq:
            return False
        return str(self.id) in chapters

    def mark_read(self, user):
        acq, chapters = self._get_read_chapters(user)
        if not acq:
            return False
        if str(self.id) not in chapters:
            chapters.append(str(self.id))
            acq.chapters_read = ','.join(chapters)
            acq.save()
    # retorna o score referente ao capitulo

    def get_completed_slider(self, user, acquired):
        pass

    def get_exam_score(self, user, acquired):
        # if hasattr(self, '_exam_score'):
        #     return self._exam_score
        qs = self.exams.filter(
            user=user,
            acquired=acquired,
        ).order_by('-started')
        if not qs.exists():
            # self._exam_score = None
            return None
        exam = qs[0]
        correct = exam.questions.filter(answer__correct=True).count()

        if exam.total > 0:
            exam_score = {
                'percent': int(Decimal(correct) / Decimal(exam.total) * 100),
                'correct': correct,
                'total': exam.total,
            }
            
        else:
            exam_score = None

        return exam_score

class Acquired(models.Model):
    token = models.CharField(max_length=5, unique=True, blank=True, null=True)
    logs = models.ManyToManyField(UserReportLog, verbose_name="logs",blank=True,related_name='acquired')
    course = models.ForeignKey(
        Course,
        verbose_name='Curso',
        related_name='acquired',on_delete=models.CASCADE,
    )

    user = models.ForeignKey(
        User,
        verbose_name=u'Usuário',on_delete=models.CASCADE,
    )

    paid = models.BooleanField(
        verbose_name='Pago:',
        default=False,
    )

    sent_conclusion_mail = models.BooleanField(
        verbose_name=u'Email de certificado enviado?',
        default=False,
    )

    made_testimony = models.BooleanField(
        verbose_name=u'Enviou depoimento?',
        default=False,
    )
    
    payment = models.OneToOneField(
        'Payment',on_delete=models.CASCADE,
        #verbose_name='Pagamento'
    )

    date_added = models.DateTimeField(
        verbose_name='Data',
        auto_now_add=True,
    )

    date_acquired = models.DateTimeField(
        verbose_name='Data adquirido',
        default=timezone.now,
    )

    date_expire = models.DateTimeField(
        verbose_name=u'Data de validade',
        default=date_expire_default,
        blank=True,
        null=True,
    )

    date_conclusion = models.DateTimeField(
        verbose_name=u'Data de conclusĂŁo',
        blank=True,
        null=True,
    )

    invalid_conclusion = models.BooleanField(
        verbose_name='Invalidar conclusĂŁo',
        default=False
    )

    justify_invalidate = models.TextField(max_length=500,
    verbose_name = 'justificar invalidar conclusĂŁo',
            null = True,
           blank = True,
    )

    chapters_read = models.CharField(
        validators=[validate_comma_separated_integer_list],
        max_length=1000,
        blank=True,
        editable=False,
    )

    certificate = models.ImageField(
        upload_to='certificates',
        storage=fs, 
        null=True,
        blank=True,
    )

    comments = models.TextField(max_length=1000,
    verbose_name = 'Observações',
            null = True,
           blank = True,
    )
    
    card = models.ImageField(
        upload_to='cards',
        storage=fs,
        null=True,
        blank=True,
    )

    sum_slider = models.BooleanField(
        verbose_name='Contabilizar Slider',
        default=False)

    sum_simulator = models.BooleanField(
        verbose_name='Contabilizar Simulador',
        default=False)

    finalized_simulator = models.BooleanField(
        verbose_name='Simulador Finalizado',
        default=False)

    percent_access_day = models.IntegerField(
        null=True,
        blank=True,
        validators=[
            MaxValueValidator(100),
            MinValueValidator(0)
        ],
        default=100,
        verbose_name='Porcentagem máxima de avaliações por dia'
    )

    chapters_per_day = models.CharField(
        choices=(
            ('1', '1'), ('2', '2'), ('3', '3'), ('4', '4'), ('5', '5'), ('6', '6'), ('7', '7'),
            ('8', '8'), ('9', '9'), ('10', '10'), ('11', '11'), ('12', '12'),
        ),
        default=2,
        max_length=14,
        null=True,
        verbose_name="NĂşmero de capĂ­tulos por dia"
    )

    chapters_log = models.CharField(
        max_length=14,
        default=0,
    )

    saturday_acess_bloqued = models.BooleanField(
        default=False,
        verbose_name='Acesso aos sábados bloqueado'
    )

    sunday_acess_bloqued = models.BooleanField(
        default=False,
        verbose_name='Acesso aos domingos bloqueado'
    )

    message_for_bloqued_weekend = models.TextField(
        blank=True,
        null=True,
        max_length=300,
        verbose_name="Mensagem para aluno quando acesso no fim de semana estiver bloqueado"
    )

    times_for_acess = models.ManyToManyField(TimesForAcess, blank=True, verbose_name="Horário de acesso do curso")

    track = models.ForeignKey(Track, blank=True, null=True,on_delete=models.CASCADE,)

    objects = AcquiredManager()

    class Meta:
        verbose_name = 'Curso Adquirido'
        verbose_name_plural = 'Cursos Adquiridos'
        ordering = (
            '-date_added',
        )

    def __unicode__(self):
        return u'%s - %s' % (self.course, self.user)

    def __str__(self):
        return u'%s - %s' % (self.course, self.user)

    @property
    def date_conclusion_7(self):
        if self.date_conclusion:
            return self.date_conclusion - timedelta(days=7)

    @property
    def registration(self):
        return self.id + 10100

    @property
    def valid(self):
        return (
            self.date_expire is None or self.date_expire > timezone.now()
        ) and self.paid

    @property
    def status(self):
        if self.paid:
            return 'Pago'
        else:
            return 'Aguardando pagamento'

    @property
    def show_sum_slider(self):
        if self.sum_slider is True:
            return True
        else:
            return False

    def get_completed_exams(self):
        result = []
        qs = self.course.chapters.all()
        for chapter in qs:
            # logger.info("chapter "+str(chapter))
            exam_score = chapter.get_exam_score(self.user,self)
            if exam_score:
                if exam_score['percent'] >= 75:
                    # logger.info("aprovado " + str(exam_score['percent']) + " cap" + str(chapter.order))
                    result.append(chapter)
                # else:
                    # logger.info("reprovado " + str(exam_score['percent'])+" cap" + str(chapter.order))

        #print(result)
        return result

   def get_not_completed_exams(self):
        result = []
        # controller = []
        # stack = int(self.chapters_per_day)
        qs = self.course.chapters.all()

        for chapter in qs:
            exam_score = chapter.get_exam_score(self.user, self)
            if not exam_score:
                result.append(chapter)

        # for i in range(stack):
        #     controller.append(result[i])

        # print(result)
        return result
class CourseShowView(CommonGet, DetailView):

    template_name = 'courses/course_show.html'
    model = Course

    def get_object(self):
        self.course = super(CourseShowView, self).get_object()
        qs = self.course.acquired.valid().filter(user=self.request.user)

        if qs.exists():
            self.object = qs[0].course
            return qs[0]

    def get_context_data(self, **kwargs):
        context = super(CourseShowView, self).get_context_data(**kwargs)
        qs = self.object.acquired.valid().filter(user=self.request.user)
        if not qs.exists():

            return redirect(
                '/cursos-online/'+self.object.slug
            )
        # reverse('courses_courses_detail', kwargs={
        #     'slug': self.object.slug
        # })
        acquired = qs[0]

        context['acquired'] = acquired
        context['score'] = acquired.get_exam_score()
        context['navinclude'] = 'courses/header_area.html'
        
        return context

    def get(self, request, slug):
        self.object = self.get_object()
        context = super(CourseShowView, self).get_context_data()

        if not 'acquired' in context:
            log.info("erro ao obter curso adquirido, redirecionando para compra "+str(self.request.user)+" slug "+str(slug))
            return redirect('/cursos-online/' + str(slug))
        else:
            acquired = context['acquired']

        log_frequency_minutes, max_inactivity_minutes = get_log_parameters()
        context['max_inactivity_minutes']= max_inactivity_minutes
        context['log_frequency_minutes'] = log_frequency_minutes
        context['sliders_url'] = settings.SLIDERS_REDIRECT_URL

        # chapters_per_day = acquired.get_libered_chapters()
        # context['chapters_per_day'] = chapters_per_day

        from datetime import date
        num = date.today().weekday()
        today = date.today()

        date_acquired = str(acquired.date_acquired)
        date_acquired_sliced = date_acquired[0:10]

        today_str = str(today)

        all_chapters = len(self.course.chapters.all())                     # NĂşm de capĂ­tulos do curso
        all_chapters_not_libered = len(acquired.get_not_completed_exams()) # NĂşm de capĂ­tulos nĂŁo completados
        chapters_completed = len(acquired.get_completed_exams())           # NĂşm de capĂ­tulos completados
        chapters_per_day = int(acquired.get_libered_chapters())                 # NĂşm de capĂ­tulos liberados

        **if date_acquired_sliced == today_str:**
**            context['chapters_per_day'] = chapters_per_day**

**        else:**
**            chapters_log = acquired.get_chapters_log()**

**            chapters_per_day += int(chapters_log)**

**            if chapters_per_day > all_chapters:**
**                chapters_per_day = all_chapters**

**            context['chapters_per_day'] = chapters_per_day**
**            print(chapters_per_day)**

**            new_acquired = Acquired.objects.get(id=acquired.id)**
**            new_acquired.chapters_log = chapters_per_day**
**            print(new_acquired.chapters_log)**
**            new_acquired.save()**
**            **
**            if new_acquired.chapters_log > int(acquired.get_libered_chapters()):**
**                new_acquired.chapters_log = int(acquired.get_libered_chapters())**
**                new_acquired.save()**

**        print(context)**
**        return self.render_to_response(context)**

In my template:

[...]

<ul>

            {% for chapter in object.course.chapters.all %}
                <li><a style="pointer-events: none" id="test{{ chapter.id }}" class="tabs-link-capter chap" href="#tabs-{{ chapter.id }}" onclick="uriRedirectAfterSlider('#tabs-{{ chapter.id }}')">CapĂ­tulo {{ forloop.counter }}</a></li>
                <!-- style="pointer-events: none" -->

                <script>
                    $("#test{{ chapter.id }}").each(function() {
                        $(this).css("color", "#7F8C8D");   
                    });

                </script>
            {% endfor %}

        {% for chapter in object.course.chapters.all|**slice:chapters_per_day** %}
                <script>
                    $("#test{{ chapter.id }}").removeAttr("style");
                </script>
            {% endfor %}

Side note:

Which “calendar day”? Your’s? The person using the site? UTC? (Note, I don’t need to know the answer to this question, but you do.)

Side note 2: I’d change “chapters_per_day” to be an integer field. You can still create a select widget for it, but it is a numerical value that you will be performing math or comparisons with.


Is there anything AJAX going on with this page? If not, I wouldn’t be styling them in JavaScript. I’d be making the determination of what chapters are available in the server and rendering them accordingly.

Basically, I’d be looking at it this way. Subtract the number of chapters started (or completed) “today” from the total number of chapters allowed to be started (or completed) “today”, and use that difference to enable the next “N” chapters.

Side note 3: You’re doing a lot of manual work with these querysets that you don’t need to do. For example, in your get_object method, you’re referring to qs[0], where you could use the first() function in your query to return the first element of the queryset. You may want to review all of the functions at QuerySet API reference | Django documentation | Django to get some ideas of what all is available to you. Likewise, you’re doing a lot of work with strings that should more appropriately be done with the proper Python objects.

1 Like

Thanks for the help. I’ll try to do this. :))

Morning!!

It can be UTC-3, doesn’t matter at first, I can do some conversions after, if necessary. :))

In this case, that’s the problem. Using Ajax or not, doens’t matter at first, beacuse I’d need to limit the number of chapters per day. For example, when the user finishes the two available chapters, how can I lock my queue of available chapters so that it’s only available tomorrow? I tried to apply a timeout, but the page would be loading infinitely, until the timeout ended, so it’s unfeasible.

When you’re rendering the page, check to see if the person is eligible to open a new chapter. You count the number of chapters used today to the number of chapters they’re able to use in a day to determine what they see. There’s no “timeout” or “lock” necessary.

1 Like