Попытка создать одноразовое вычисляемое поле в модели Django

Строю свое первое приложение на Django и столкнулся с проблемой. У меня есть модель Django, которая создает объекты Job, и я хочу, чтобы каждый код задания был уникальным и автоматически генерировался в определенном формате. Формат следующий: aaaMMnnYYYY, где aaa - это 3-буквенный идентификатор клиента, который мы задаем, nn - это счетчик, который представляет n-ое задание от этого клиента в этом месяце, а MM и YYYY - это месяц и год соответственно. Например, для 3-го задания от клиента "AIE" в феврале 2023 года идентификатор будет AIE02032023.

Использование вычисляемого поля с декоратором @property приводит к постоянному обновлению поля, поэтому я пытаюсь сделать это путем модификации метода save(). Есть также связанный объект Cost, который имеет атрибуты Job через Foreign Key. Сейчас, когда я добавляю Cost, "итерирующая" часть кода задания выполняет итерации, изменяя код задания, что вызывает ошибки уникальности, а также ошибки URL (я использую код задания в URLConf.

).

В качестве побочного примечания, я также хотел бы иметь возможность переопределять код задания. Есть ли способ установить флаги в модели, такие как job_code_overridden = False и т.д.?

Вот соответствующий код, сообщите мне, что еще вам нужно увидеть.

models.py:

class Job(models.Model):
    
    job_name = models.CharField(max_length=50, default='New Job')
    client = models.ForeignKey(Client, on_delete=models.CASCADE)
    job_code = models.CharField(max_length=15, unique=True,)

    def get_job_code(self):
        '''
        I only want this to run once
        Format abcMMnnYYYY

        '''
        jc = ''
        prefix = self.client.job_code_prefix
        month = str(str(self.job_date).split('-')[1])
        identifier = len(Job.objects.filter(job_date__contains = f'-{month}-',
                                    client__job_code_prefix = prefix)) + 2
        year = str(str(self.job_date).split('-')[0])
        jc = f'{prefix}{month}{identifier:02d}{year}'

        return jc


    @property
    def total_cost(self):
        all_costs = Cost.objects.filter(job__job_code = self.job_code)
        total = 0
        if all_costs:
            for cost in all_costs:
                total += cost.amount
        return total

        # Is there a way to add something like the flags in the commented-out code here?
    def save(self, *args, **kwargs):
        # if not self.job_code_fixed:
        if self.job_code != self.get_job_code():
             self.job_code = self.get_job_code()
             # self.job_code_fixed = True
        super().save(*args, **kwargs)

costsheet.py:

class costsheetView(ListView):
    template_name = "main_app/costsheet.html"
    form_class = CostForm
    model = Cost
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        current_job_code = self.kwargs['job_code']
        currentJob = Job.objects.get(job_code=current_job_code)
        return context

    def get(self, request, *args, **kwargs):
        cost_form = self.form_class()
        current_job_code = self.kwargs['job_code']
        currentJob = Job.objects.get(job_code=current_job_code)
        all_costs = Cost.objects.filter(job__job_code = current_job_code)
        return render(request, self.template_name, {'cost_form':cost_form, 'currentJob':currentJob,'all_costs':all_costs})

    def post(self, request, *args, **kwargs):
        cost_form = self.form_class(request.POST)
        current_job_code = self.kwargs['job_code']
        currentJob = Job.objects.get(job_code=current_job_code)
        messages = []
        errors = ''
        if cost_form.is_valid():
            instance = cost_form.save()
            instance.job = currentJob
            instance.save()
            currentJob.vendors.add(instance.vendor)
            currentJob.save()
            messages.append(f'cost added, job date: {currentJob.job_date}')
        else: 
            print('oops')
            print(cost_form.errors)
            errors = cost_form.errors

        all_costs = Cost.objects.filter(job__job_code = current_job_code)
        return render(request, self.template_name, {'cost_form':cost_form, 
                                                             'currentJob':currentJob, 
                                                    'errors':errors, 
                                                    'messages':messages,
                                                    'all_costs':all_costs,
                                                    })

Наконец, в методе save() я знаю, что могу сделать что-то вроде

if job_code != get_job_code():
   job_code = get_job_code()

...но "месяц" задания часто меняется в течение жизни задания, и если я запущу get_job_code() после изменения месяца, то код задания снова изменится.

Я проделал то, что пробовал выше^

Возможным решением является добавление дополнительного поля флага job_code_fixed к модели Job. Флаг job_code должен генерироваться только один раз при создании объекта Job, а не при последующих сохранениях. Этого можно достичь, установив job_code_fixed в True после того, как job_code будет сгенерировано в методе save, и генерируя job_code только в том случае, если job_code_fixed будет False.

Вот обновленная версия кода:

class Job(models.Model):
    
    job_name = models.CharField(max_length=50, default='New Job')
    client = models.ForeignKey(Client, on_delete=models.CASCADE)
    job_code = models.CharField(max_length=15, unique=True,)
    job_code_fixed = models.BooleanField(default=False)

    def get_job_code(self):
        '''
        I only want this to run once
        Format abcMMnnYYYY

        '''
        jc = ''
        prefix = self.client.job_code_prefix
        month = str(str(self.job_date).split('-')[1])
        identifier = len(Job.objects.filter(job_date__contains = f'-{month}-',
                                    client__job_code_prefix = prefix)) + 2
        year = str(str(self.job_date).split('-')[0])
        jc = f'{prefix}{month}{identifier:02d}{year}'

        return jc


    @property
    def total_cost(self):
        all_costs = Cost.objects.filter(job__job_code = self.job_code)
        total = 0
        if all_costs:
            for cost in all_costs:
                total += cost.amount
        return total

    def save(self, *args, **kwargs):
        if not self.job_code_fixed:
            self.job_code = self.get_job_code()
            self.job_code_fixed = True
        super().save(*args, **kwargs)

Добавив флаг job_code_fixed, job_code будет генерироваться только один раз и никогда не будет изменяться, решая проблему с изменением кодов заданий, вызывающих ошибки уникальности и URL.

В итоге я просто переопределил метод save(), используя флаг job_code_is_fixed boolean, чтобы job_code не обновлялся.

class Job(models.Model):
    #...
    job_code = models.CharField(
        max_length=15, unique=True, blank=True, null=True
        )
    job_code_is_fixed = models.BooleanField(default=False)
    #...

    def set_job_code(self, prefix=None, year=None, month=None):
        '''
        Create a job code in the following format:
        {prefix}{month:02d}{i:02d}{year}

        e.g. APL06012023:
        prefix: APL
        month: 06
        i: 01 
        year: 2023

        params:
        prefix: job code prefix
        year: the year to be used in the job code
        month: the month to be used in the job code
        i: iterator for generating unique job codes
        passable args are auto generated, so they default to None unless passed in.

        This function will run in the save() method as long as job_code_is_fixed is False
        '''

        jc = ''
        prefix = self.client.job_code_prefix if prefix is None else prefix
        month = int(timezone.now().month) if month is None else month
        year = int(timezone.now().year) if year is None else year

        i = 1
        while jc == '' and i <= 99:

            temp_jc = f'{prefix}{month:02d}{i:02d}{year}'
            if not Job.objects.filter(job_code=temp_jc).exists():
                jc = temp_jc
                return jc
            else:
                i += 1

        if jc == '':
            # Handle error

    def save(self, *args, **kwargs):
    # Auto-generate the job code
        if not self.job_code_is_fixed:
            self.job_code = self.set_job_code()
            self.job_code_is_fixed = True
        # ...
        super().save(*args, **kwargs)
Вернуться на верх