mirror of
https://github.com/makeplane/plane.git
synced 2026-07-13 14:01:45 +02:00
[WEB-4393] chore: added recurring work item activity model (#3847)
* chore: added recurring work item activity model * chore: added a better description
This commit is contained in:
committed by
GitHub
parent
0a6f160530
commit
b5ebf625ee
@@ -0,0 +1,169 @@
|
||||
# Generated by Django 4.2.22 on 2025-08-05 15:21
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("django_celery_beat", "0018_improve_crontab_helptext"),
|
||||
("db", "0100_profile_has_marketing_email_consent_and_more"),
|
||||
("ee", "0041_workitemtemplate_parent_workitem_template_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="recurringworkitemtask",
|
||||
name="interval_type",
|
||||
field=models.CharField(default="monthly", max_length=20),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="recurringworkitemtask",
|
||||
name="periodic_task",
|
||||
field=models.OneToOneField(
|
||||
blank=True,
|
||||
help_text="Keeps Beat in sync with this row",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="recurring_workitem_task",
|
||||
to="django_celery_beat.periodictask",
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="RecurringWorkItemTaskActivity",
|
||||
fields=[
|
||||
(
|
||||
"created_at",
|
||||
models.DateTimeField(auto_now_add=True, verbose_name="Created At"),
|
||||
),
|
||||
(
|
||||
"updated_at",
|
||||
models.DateTimeField(
|
||||
auto_now=True, verbose_name="Last Modified At"
|
||||
),
|
||||
),
|
||||
(
|
||||
"deleted_at",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="Deleted At"
|
||||
),
|
||||
),
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
db_index=True,
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"verb",
|
||||
models.CharField(
|
||||
default="created", max_length=255, verbose_name="Action"
|
||||
),
|
||||
),
|
||||
(
|
||||
"field",
|
||||
models.CharField(
|
||||
blank=True, max_length=255, null=True, verbose_name="Field Name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"old_value",
|
||||
models.TextField(blank=True, null=True, verbose_name="Old Value"),
|
||||
),
|
||||
(
|
||||
"new_value",
|
||||
models.TextField(blank=True, null=True, verbose_name="New Value"),
|
||||
),
|
||||
("old_identifier", models.UUIDField(null=True)),
|
||||
("new_identifier", models.UUIDField(null=True)),
|
||||
("epoch", models.FloatField(null=True)),
|
||||
(
|
||||
"actor",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="recurring_workitem_task_activities",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"created_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_created_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Created By",
|
||||
),
|
||||
),
|
||||
(
|
||||
"project",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="project_%(class)s",
|
||||
to="db.project",
|
||||
),
|
||||
),
|
||||
(
|
||||
"property",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="recurring_workitem_task_activities",
|
||||
to="ee.issueproperty",
|
||||
),
|
||||
),
|
||||
(
|
||||
"recurring_workitem_task",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="recurring_workitem_task_activities",
|
||||
to="ee.recurringworkitemtask",
|
||||
),
|
||||
),
|
||||
(
|
||||
"recurring_workitem_task_log",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="recurring_workitem_task_activities",
|
||||
to="ee.recurringworkitemtasklog",
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_updated_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Last Modified By",
|
||||
),
|
||||
),
|
||||
(
|
||||
"workspace",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="workspace_%(class)s",
|
||||
to="db.workspace",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Recurring Workitem Task Activity",
|
||||
"verbose_name_plural": "Recurring Workitem Task Activities",
|
||||
"db_table": "recurring_workitem_task_activities",
|
||||
"ordering": ("-created_at",),
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -104,4 +104,5 @@ from .page import PageUser
|
||||
from .recurring import (
|
||||
RecurringWorkitemTask,
|
||||
RecurringWorkitemTaskLog,
|
||||
RecurringWorkItemTaskActivity,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import json
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django_celery_beat.models import PeriodicTask
|
||||
from django.core.exceptions import ValidationError
|
||||
from django_celery_beat.models import PeriodicTask, CrontabSchedule
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import ProjectBaseModel
|
||||
@@ -13,6 +17,40 @@ class RecurringWorkitemTask(ProjectBaseModel):
|
||||
Keeps a one-to-one link to Celery Beat's PeriodicTask.
|
||||
"""
|
||||
|
||||
# Constants for interval types
|
||||
INTERVAL_DAILY = "daily"
|
||||
INTERVAL_WEEKLY = "weekly"
|
||||
INTERVAL_MONTHLY = "monthly"
|
||||
INTERVAL_YEARLY = "yearly"
|
||||
|
||||
# Temp intervals for testing
|
||||
INVERVAL_MINUTLY = "minutly"
|
||||
INTERVAL_HOURLY = "hourly"
|
||||
|
||||
INTERVAL_CHOICES = [
|
||||
(INTERVAL_DAILY, "Daily"),
|
||||
(INTERVAL_WEEKLY, "Weekly"),
|
||||
(INTERVAL_MONTHLY, "Monthly"),
|
||||
(INTERVAL_YEARLY, "Yearly"),
|
||||
(INVERVAL_MINUTLY, "Minutly"),
|
||||
(INTERVAL_HOURLY, "Hourly"),
|
||||
]
|
||||
|
||||
# Celery task name constant
|
||||
CELERY_TASK_NAME = (
|
||||
"plane.ee.bgtasks.recurring_work_item_task.create_work_item_from_template"
|
||||
)
|
||||
|
||||
# Cron expression constants
|
||||
CRON_WILDCARD = "*"
|
||||
CRON_FIELDS_COUNT = 5
|
||||
|
||||
# Day of week conversion (Python weekday to cron)
|
||||
# Python: 0=Monday, 6=Sunday
|
||||
# Cron: 0=Sunday, 6=Saturday
|
||||
PYTHON_SUNDAY = 6
|
||||
CRON_SUNDAY = 0
|
||||
|
||||
workitem_blueprint = models.ForeignKey(
|
||||
"ee.WorkitemTemplate",
|
||||
on_delete=models.CASCADE,
|
||||
@@ -23,6 +61,10 @@ class RecurringWorkitemTask(ProjectBaseModel):
|
||||
end_at = models.DateTimeField(
|
||||
null=True, blank=True, help_text="Cut-off after which no runs occur (UTC)"
|
||||
)
|
||||
interval_type = models.CharField(
|
||||
max_length=20,
|
||||
default=INTERVAL_MONTHLY,
|
||||
)
|
||||
interval_seconds = models.PositiveIntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
@@ -39,7 +81,7 @@ class RecurringWorkitemTask(ProjectBaseModel):
|
||||
)
|
||||
periodic_task = models.OneToOneField(
|
||||
PeriodicTask,
|
||||
on_delete=models.CASCADE,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="recurring_workitem_task",
|
||||
@@ -56,33 +98,289 @@ class RecurringWorkitemTask(ProjectBaseModel):
|
||||
return f"<{self.workitem_blueprint.name} {self.start_at}>"
|
||||
|
||||
def clean(self):
|
||||
"""Validate that either interval_seconds or cron_expression is provided, not both"""
|
||||
from django.core.exceptions import ValidationError
|
||||
"""Validate required fields"""
|
||||
if not self.cron_expression:
|
||||
raise ValidationError("cron_expression is required")
|
||||
if not self.start_at:
|
||||
raise ValidationError("start_at is required")
|
||||
|
||||
if self.interval_seconds is not None and self.cron_expression:
|
||||
@classmethod
|
||||
def generate_cron_expression(cls, start_at, interval_type):
|
||||
"""
|
||||
Generate cron expression based on start_at datetime and interval_type.
|
||||
|
||||
Args:
|
||||
start_at (datetime): The datetime when the task should start
|
||||
interval_type (str): One of 'second', 'daily', 'week', 'month', 'year'
|
||||
|
||||
Returns:
|
||||
str: Cron expression in format "minute hour day month day_of_week"
|
||||
|
||||
Raises:
|
||||
ValidationError: If interval_type is invalid
|
||||
"""
|
||||
if not start_at:
|
||||
return None
|
||||
|
||||
# Validate interval type
|
||||
valid_intervals = [choice[0] for choice in cls.INTERVAL_CHOICES]
|
||||
if interval_type not in valid_intervals:
|
||||
raise ValidationError(
|
||||
"Cannot specify both interval_seconds and cron_expression"
|
||||
f"Invalid interval_type: {interval_type}. Must be one of {valid_intervals}"
|
||||
)
|
||||
|
||||
if self.interval_seconds is None and not self.cron_expression:
|
||||
raise ValidationError(
|
||||
"Must specify either interval_seconds or cron_expression"
|
||||
)
|
||||
# Extract datetime components for cron-based intervals
|
||||
minute = start_at.minute
|
||||
hour = start_at.hour
|
||||
day = start_at.day
|
||||
month = start_at.month
|
||||
|
||||
if self.interval_seconds is not None and self.interval_seconds < 60:
|
||||
raise ValidationError("interval_seconds must be at least 60 seconds")
|
||||
# Convert Python weekday to cron format
|
||||
cron_day_of_week = cls._convert_python_weekday_to_cron(start_at.weekday())
|
||||
|
||||
# Generate cron expression based on interval type
|
||||
if interval_type == cls.INTERVAL_DAILY:
|
||||
return f"{minute} {hour} {cls.CRON_WILDCARD} {cls.CRON_WILDCARD} {cls.CRON_WILDCARD}"
|
||||
elif interval_type == cls.INTERVAL_WEEKLY:
|
||||
return f"{minute} {hour} {cls.CRON_WILDCARD} {cls.CRON_WILDCARD} {cron_day_of_week}"
|
||||
elif interval_type == cls.INTERVAL_MONTHLY:
|
||||
return f"{minute} {hour} {day} {cls.CRON_WILDCARD} {cls.CRON_WILDCARD}"
|
||||
elif interval_type == cls.INTERVAL_YEARLY:
|
||||
return f"{minute} {hour} {day} {month} {cls.CRON_WILDCARD}"
|
||||
elif interval_type == cls.INVERVAL_MINUTLY:
|
||||
return f"{cls.CRON_WILDCARD} {cls.CRON_WILDCARD} {cls.CRON_WILDCARD} {cls.CRON_WILDCARD} {cls.CRON_WILDCARD}"
|
||||
elif interval_type == cls.INTERVAL_HOURLY:
|
||||
return f"{minute} {cls.CRON_WILDCARD} {cls.CRON_WILDCARD} {cls.CRON_WILDCARD} {cls.CRON_WILDCARD}"
|
||||
|
||||
@classmethod
|
||||
def _convert_python_weekday_to_cron(cls, python_weekday):
|
||||
"""Convert Python weekday (0=Monday, 6=Sunday) to cron format (0=Sunday, 6=Saturday)"""
|
||||
return (
|
||||
cls.CRON_SUNDAY
|
||||
if python_weekday == cls.PYTHON_SUNDAY
|
||||
else python_weekday + 1
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.clean()
|
||||
"""Override save to generate cron expression and manage PeriodicTask synchronization"""
|
||||
# Auto-generate cron expression from interval_type and start_at
|
||||
if self.interval_type and self.start_at:
|
||||
self.cron_expression = self.generate_cron_expression(
|
||||
self.start_at, self.interval_type
|
||||
)
|
||||
|
||||
# Create or update the periodic task here
|
||||
self.clean()
|
||||
super().save(*args, **kwargs)
|
||||
self._sync_periodic_task()
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""Override delete to clean up associated PeriodicTask"""
|
||||
if self.periodic_task:
|
||||
self.periodic_task.delete()
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
def _sync_periodic_task(self):
|
||||
"""Create or update the associated PeriodicTask for Beat scheduling"""
|
||||
task_name = self._generate_task_name()
|
||||
task_args, task_kwargs = self._prepare_task_arguments()
|
||||
|
||||
cron_schedule = self._get_or_create_cron_schedule()
|
||||
|
||||
if self.periodic_task:
|
||||
self._update_existing_periodic_task(
|
||||
task_name, task_args, task_kwargs, cron_schedule
|
||||
)
|
||||
else:
|
||||
self._create_new_periodic_task(
|
||||
task_name, task_args, task_kwargs, cron_schedule
|
||||
)
|
||||
|
||||
def _generate_task_name(self):
|
||||
"""Generate a unique task name for the periodic task"""
|
||||
return f"rwit-{self.pk}-{self.workitem_blueprint.name}"
|
||||
|
||||
def _prepare_task_arguments(self):
|
||||
"""Prepare arguments for the celery task"""
|
||||
task_args = []
|
||||
task_kwargs = {"recurring_workitem_task_id": str(self.pk)}
|
||||
return json.dumps(task_args), json.dumps(task_kwargs)
|
||||
|
||||
def _update_existing_periodic_task(
|
||||
self, task_name, task_args, task_kwargs, cron_schedule
|
||||
):
|
||||
"""Update an existing PeriodicTask"""
|
||||
periodic_task = self.periodic_task
|
||||
periodic_task.name = task_name
|
||||
periodic_task.task = self.CELERY_TASK_NAME
|
||||
periodic_task.args = task_args
|
||||
periodic_task.kwargs = task_kwargs
|
||||
periodic_task.enabled = self.enabled
|
||||
periodic_task.start_time = self.start_at
|
||||
periodic_task.expires = self.end_at
|
||||
periodic_task.crontab = cron_schedule
|
||||
periodic_task.interval = None
|
||||
periodic_task.save()
|
||||
|
||||
def _create_new_periodic_task(
|
||||
self, task_name, task_args, task_kwargs, cron_schedule
|
||||
):
|
||||
"""Create a new PeriodicTask"""
|
||||
periodic_task = PeriodicTask.objects.create(
|
||||
name=task_name,
|
||||
task=self.CELERY_TASK_NAME,
|
||||
args=task_args,
|
||||
kwargs=task_kwargs,
|
||||
enabled=self.enabled,
|
||||
start_time=self.start_at,
|
||||
expires=self.end_at,
|
||||
crontab=cron_schedule,
|
||||
interval=None,
|
||||
)
|
||||
self.periodic_task = periodic_task
|
||||
super().save(update_fields=["periodic_task"])
|
||||
|
||||
def _get_or_create_cron_schedule(self):
|
||||
"""Get or create CrontabSchedule for cron-based scheduling"""
|
||||
if not self.cron_expression:
|
||||
return None
|
||||
|
||||
cron_parts = self._parse_cron_expression()
|
||||
project_timezone = self.project.timezone
|
||||
|
||||
schedule, created = CrontabSchedule.objects.get_or_create(
|
||||
minute=cron_parts["minute"],
|
||||
hour=cron_parts["hour"],
|
||||
day_of_month=cron_parts["day_of_month"],
|
||||
month_of_year=cron_parts["month_of_year"],
|
||||
day_of_week=cron_parts["day_of_week"],
|
||||
timezone=project_timezone,
|
||||
)
|
||||
return schedule
|
||||
|
||||
def _parse_cron_expression(self):
|
||||
"""Parse cron expression into components"""
|
||||
parts = self.cron_expression.split()
|
||||
if len(parts) != self.CRON_FIELDS_COUNT:
|
||||
raise ValidationError(f"Invalid cron expression: {self.cron_expression}")
|
||||
|
||||
return {
|
||||
"minute": parts[0],
|
||||
"hour": parts[1],
|
||||
"day_of_month": parts[2],
|
||||
"month_of_year": parts[3],
|
||||
"day_of_week": parts[4],
|
||||
}
|
||||
|
||||
def enable(self):
|
||||
"""Enable the recurring task"""
|
||||
self.enabled = True
|
||||
self.save()
|
||||
|
||||
def disable(self):
|
||||
"""Disable the recurring task"""
|
||||
self.enabled = False
|
||||
self.save()
|
||||
|
||||
def get_next_run_time(self):
|
||||
"""Get the next scheduled run time"""
|
||||
if self.periodic_task:
|
||||
return self.periodic_task.get_next_run_time()
|
||||
return None
|
||||
|
||||
def _detect_interval_type_from_cron(self):
|
||||
"""
|
||||
Detect interval type from cron expression pattern.
|
||||
|
||||
Returns:
|
||||
str: One of 'daily', 'week', 'month', 'year', or None if pattern doesn't match
|
||||
"""
|
||||
if not self.cron_expression:
|
||||
return None
|
||||
|
||||
try:
|
||||
cron_parts = self._parse_cron_expression()
|
||||
except ValidationError:
|
||||
return None
|
||||
|
||||
# Check patterns to determine interval type
|
||||
if self._is_daily_pattern(cron_parts):
|
||||
return self.INTERVAL_DAILY
|
||||
elif self._is_weekly_pattern(cron_parts):
|
||||
return self.INTERVAL_WEEKLY
|
||||
elif self._is_monthly_pattern(cron_parts):
|
||||
return self.INTERVAL_MONTHLY
|
||||
elif self._is_yearly_pattern(cron_parts):
|
||||
return self.INTERVAL_YEARLY
|
||||
|
||||
return None
|
||||
|
||||
def _is_daily_pattern(self, cron_parts):
|
||||
"""Check if cron pattern represents daily recurrence"""
|
||||
return (
|
||||
cron_parts["day_of_month"] == self.CRON_WILDCARD
|
||||
and cron_parts["month_of_year"] == self.CRON_WILDCARD
|
||||
and cron_parts["day_of_week"] == self.CRON_WILDCARD
|
||||
)
|
||||
|
||||
def _is_weekly_pattern(self, cron_parts):
|
||||
"""Check if cron pattern represents weekly recurrence"""
|
||||
return (
|
||||
cron_parts["day_of_month"] == self.CRON_WILDCARD
|
||||
and cron_parts["month_of_year"] == self.CRON_WILDCARD
|
||||
and cron_parts["day_of_week"] != self.CRON_WILDCARD
|
||||
)
|
||||
|
||||
def _is_monthly_pattern(self, cron_parts):
|
||||
"""Check if cron pattern represents monthly recurrence"""
|
||||
return (
|
||||
cron_parts["day_of_month"] != self.CRON_WILDCARD
|
||||
and cron_parts["month_of_year"] == self.CRON_WILDCARD
|
||||
and cron_parts["day_of_week"] == self.CRON_WILDCARD
|
||||
)
|
||||
|
||||
def _is_yearly_pattern(self, cron_parts):
|
||||
"""Check if cron pattern represents yearly recurrence"""
|
||||
return (
|
||||
cron_parts["day_of_month"] != self.CRON_WILDCARD
|
||||
and cron_parts["month_of_year"] != self.CRON_WILDCARD
|
||||
and cron_parts["day_of_week"] == self.CRON_WILDCARD
|
||||
)
|
||||
|
||||
@property
|
||||
def schedule_description(self):
|
||||
"""Get a human-readable description of the schedule"""
|
||||
if not self.cron_expression:
|
||||
return "No schedule configured"
|
||||
|
||||
interval_type = self._detect_interval_type_from_cron()
|
||||
|
||||
if interval_type == self.INTERVAL_DAILY:
|
||||
return (
|
||||
f"Every day at {self.start_at.strftime('%H:%M')}"
|
||||
if self.start_at
|
||||
else "Every day"
|
||||
)
|
||||
elif interval_type == self.INTERVAL_WEEKLY:
|
||||
return (
|
||||
f"Every week on {self.start_at.strftime('%A')} at {self.start_at.strftime('%H:%M')}"
|
||||
if self.start_at
|
||||
else f"Cron: {self.cron_expression}"
|
||||
)
|
||||
elif interval_type == self.INTERVAL_MONTHLY:
|
||||
return (
|
||||
f"Every month on day {self.start_at.day} at {self.start_at.strftime('%H:%M')}"
|
||||
if self.start_at
|
||||
else f"Cron: {self.cron_expression}"
|
||||
)
|
||||
elif interval_type == self.INTERVAL_YEARLY:
|
||||
return (
|
||||
f"Every year on {self.start_at.strftime('%B %d')} at {self.start_at.strftime('%H:%M')}"
|
||||
if self.start_at
|
||||
else f"Cron: {self.cron_expression}"
|
||||
)
|
||||
else:
|
||||
return f"Cron: {self.cron_expression}"
|
||||
|
||||
|
||||
class RecurringWorkitemTaskLog(ProjectBaseModel):
|
||||
"""
|
||||
@@ -132,3 +430,62 @@ class RecurringWorkitemTaskLog(ProjectBaseModel):
|
||||
|
||||
def __str__(self):
|
||||
return f"<{self.recurring_task.workitem_blueprint.name} {self.status} {self.started_at}>"
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
"""Get the duration of the task execution"""
|
||||
if self.finished_at and self.started_at:
|
||||
return self.finished_at - self.started_at
|
||||
return None
|
||||
|
||||
|
||||
class RecurringWorkItemTaskActivity(ProjectBaseModel):
|
||||
"""
|
||||
One row per execution; records status, timestamps, and the concrete Work Item produced.
|
||||
Provides audit trail for recurring workitem creation.
|
||||
"""
|
||||
|
||||
recurring_workitem_task = models.ForeignKey(
|
||||
RecurringWorkitemTask,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="recurring_workitem_task_activities",
|
||||
)
|
||||
recurring_workitem_task_log = models.ForeignKey(
|
||||
RecurringWorkitemTaskLog,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="recurring_workitem_task_activities",
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
verb = models.CharField(max_length=255, verbose_name="Action", default="created")
|
||||
field = models.CharField(
|
||||
max_length=255, verbose_name="Field Name", blank=True, null=True
|
||||
)
|
||||
property = models.ForeignKey(
|
||||
"ee.IssueProperty",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="recurring_workitem_task_activities",
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
old_value = models.TextField(verbose_name="Old Value", blank=True, null=True)
|
||||
new_value = models.TextField(verbose_name="New Value", blank=True, null=True)
|
||||
actor = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
related_name="recurring_workitem_task_activities",
|
||||
)
|
||||
old_identifier = models.UUIDField(null=True)
|
||||
new_identifier = models.UUIDField(null=True)
|
||||
epoch = models.FloatField(null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Recurring Workitem Task Activity"
|
||||
verbose_name_plural = "Recurring Workitem Task Activities"
|
||||
db_table = "recurring_workitem_task_activities"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
"""Return recurring workitem task log of the activity"""
|
||||
return str(self.recurring_workitem_task_log)
|
||||
|
||||
Reference in New Issue
Block a user