class WebhookLog(models.Model): webhook = models.ForeignKey(Webhook, null=False, blank=False, related_name="logs") url = models.URLField(null=False, blank=False, verbose_name=_("URL")) status = models.IntegerField(null=False, blank=False, verbose_name=_("status code")) request_data = JSONField(null=False, blank=False, verbose_name=_("request data")) request_headers = JSONField(null=False, blank=False, verbose_name=_("request headers"), default={}) response_data = models.TextField(null=False, blank=False, verbose_name=_("response data")) response_headers = JSONField(null=False, blank=False, verbose_name=_("response headers"), default={}) duration = models.FloatField(null=False, blank=False, verbose_name=_("duration"), default=0) created = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['-created', '-id']
class StorageEntry(models.Model): owner = models.ForeignKey(settings.AUTH_USER_MODEL, blank=False, null=False, related_name="storage_entries", verbose_name=_("owner")) created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False, verbose_name=_("created date")) modified_date = models.DateTimeField(auto_now=True, null=False, blank=False, verbose_name=_("modified date")) key = models.CharField(max_length=255, null=False, blank=False, verbose_name=_("key")) value = JSONField(blank=True, default=None, null=True, verbose_name=_("value")) class Meta: verbose_name = "storage entry" verbose_name_plural = "storages entries" unique_together = ("owner", "key") ordering = ["owner", "key"]
class WebNotification(models.Model): created = models.DateTimeField(default=timezone.now, db_index=True) read = models.DateTimeField(default=None, null=True) user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="web_notifications") event_type = models.PositiveIntegerField() data = JSONField()
class AuthData(models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="auth_data") key = models.SlugField(max_length=50) value = models.CharField(max_length=300) extra = JSONField() class Meta: unique_together = ["key", "value"]
class AbstractCustomAttributesValues(OCCModelMixin, models.Model): attributes_values = JSONField( null=False, blank=False, default={}, verbose_name=_("values") ) class Meta: abstract = True ordering = ["id"]
class AbstractCustomAttribute(models.Model): name = models.CharField(null=False, blank=False, max_length=64, verbose_name=_("name")) description = models.TextField(null=False, blank=True, verbose_name=_("description")) type = models.CharField(null=False, blank=False, max_length=16, choices=choices.TYPES_CHOICES, default=choices.TEXT_TYPE, verbose_name=_("type")) order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms, verbose_name=_("order")) project = models.ForeignKey( "projects.Project", null=False, blank=False, related_name="%(class)ss", verbose_name=_("project"), on_delete=models.CASCADE, ) extra = JSONField(blank=True, default=None, null=True) created_date = models.DateTimeField(null=False, blank=False, default=timezone.now, verbose_name=_("created date")) modified_date = models.DateTimeField(null=False, blank=False, verbose_name=_("modified date")) _importing = None class Meta: abstract = True ordering = ["project", "order", "name"] unique_together = ("project", "name") def __str__(self): return self.name def save(self, *args, **kwargs): if not self._importing or not self.modified_date: self.modified_date = timezone.now() return super().save(*args, **kwargs)
class CumulativeFlowDiagram(models.Model): project = models.ForeignKey("projects.Project", null=False, blank=False, related_name="cumulative_flow_diagram_stats", verbose_name=_("project")) data = JSONField(null=False, blank=False, verbose_name=_("data")) created_date = models.DateField(null=False, blank=False, verbose_name=_("created date"), default=datetime.date.today) class Meta: verbose_name = "cumulative flow diagram" verbose_name_plural = "cumulative flow diagrams" ordering = ["created_date", "project_id"]
class BurnupChart(models.Model): project = models.ForeignKey("projects.Project", null=False, blank=False, related_name="burnup_chart_stats", verbose_name=_("project")) data = JSONField(null=False, blank=False, verbose_name=_("data")) created_date = models.DateField(null=False, blank=False, verbose_name=_("created date"), default=datetime.date.today) class Meta: verbose_name = "burnup chart" verbose_name_plural = "burnup charts" ordering = ["created_date", "project_id"]
class Timeline(models.Model): content_type = models.ForeignKey(ContentType, related_name="content_type_timelines", on_delete=models.CASCADE) object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') namespace = models.CharField(max_length=250, default="default", db_index=True) event_type = models.CharField(max_length=250, db_index=True) project = models.ForeignKey(Project, null=True, on_delete=models.CASCADE) data = JSONField() data_content_type = models.ForeignKey(ContentType, related_name="data_timelines", on_delete=models.CASCADE) created = models.DateTimeField(default=timezone.now, db_index=True) class Meta: indexes = [ models.Index(fields=['namespace', '-created']), models.Index(fields=['content_type', 'object_id', '-created']), ]
class Timeline(models.Model): content_type = models.ForeignKey(ContentType, related_name="content_type_timelines") object_id = models.PositiveIntegerField() content_object = GenericForeignKey("content_type", "object_id") namespace = models.CharField(max_length=250, default="default", db_index=True) event_type = models.CharField(max_length=250, db_index=True) project = models.ForeignKey(Project, null=True) data = JSONField() data_content_type = models.ForeignKey(ContentType, related_name="data_timelines") created = models.DateTimeField(default=timezone.now, db_index=True) class Meta: indexes = [ models.Index(fields=["namespace", "-created"]), models.Index(fields=["content_type", "object_id", "-created"]), ]
class VelocityChart(models.Model): project = models.ForeignKey("projects.Project", null=False, blank=False, related_name="velocity_chart_stats", verbose_name=_("project")) project_sprints_velocities = JSONField( null=False, blank=False, verbose_name=_("project sprints velocities")) created_date = models.DateField(null=False, blank=False, verbose_name=_("created date"), default=datetime.date.today) class Meta: verbose_name = "velocity chart" verbose_name_plural = "velocity charts" ordering = ["created_date", "project_id"]
class Timeline(models.Model): content_type = models.ForeignKey(ContentType, related_name="content_type_timelines") object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') namespace = models.CharField(max_length=250, default="default", db_index=True) event_type = models.CharField(max_length=250, db_index=True) project = models.ForeignKey(Project, null=True) data = JSONField() data_content_type = models.ForeignKey(ContentType, related_name="data_timelines") created = models.DateTimeField(default=timezone.now, db_index=True) class Meta: index_together = [ ('content_type', 'object_id', 'namespace'), ]
class HistoryEntry(models.Model): """ Domain model that represents a history entry storage table. It is used for store object changes and comments. """ id = models.CharField(primary_key=True, max_length=255, unique=True, editable=False, default=_generate_uuid) project = models.ForeignKey("projects.Project") user = JSONField(null=True, blank=True, default=None) created_at = models.DateTimeField(default=timezone.now) type = models.SmallIntegerField(choices=HISTORY_TYPE_CHOICES) key = models.CharField(max_length=255, null=True, default=None, blank=True, db_index=True) # Stores the last diff diff = JSONField(null=True, blank=True, default=None) # Stores the values_diff cache values_diff_cache = JSONField(null=True, blank=True, default=None) # Stores the last complete frozen object snapshot snapshot = JSONField(null=True, blank=True, default=None) # Stores a values of all identifiers used in values = JSONField(null=True, blank=True, default=None) # Stores a comment comment = models.TextField(blank=True) comment_html = models.TextField(blank=True) delete_comment_date = models.DateTimeField(null=True, blank=True, default=None) delete_comment_user = JSONField(null=True, blank=True, default=None) # Historic version of comments comment_versions = JSONField(null=True, blank=True, default=None) edit_comment_date = models.DateTimeField(null=True, blank=True, default=None) # Flag for mark some history entries as # hidden. Hidden history entries are important # for save but not important to preview. # Order fields are the good example of this fields. is_hidden = models.BooleanField(default=False) # Flag for mark some history entries as complete # snapshot. The rest are partial snapshot. is_snapshot = models.BooleanField(default=False) _importing = None _owner = None _prefetched_owner = False @cached_property def is_change(self): return self.type == HistoryType.change @cached_property def is_create(self): return self.type == HistoryType.create @cached_property def is_delete(self): return self.type == HistoryType.delete @property def owner(self): if not self._prefetched_owner: pk = self.user["pk"] model = get_user_model() try: owner = model.objects.get(pk=pk) except model.DoesNotExist: owner = None self.prefetch_owner(owner) return self._owner def prefetch_owner(self, owner): self._owner = owner self._prefetched_owner = True def attach_user_info_to_comment_versions(self): if not self.comment_versions: return from taiga.users.serializers import UserSerializer user_ids = [ v["user"]["id"] for v in self.comment_versions if "user" in v and "id" in v["user"] ] users_by_id = { u.id: u for u in get_user_model().objects.filter(id__in=user_ids) } for version in self.comment_versions: user = users_by_id.get(version["user"]["id"], None) if user: version["user"] = UserSerializer(user).data @property def values_diff(self): if self.values_diff_cache is not None: return self.values_diff_cache result = {} users_keys = ["assigned_to", "owner"] def resolve_diff_value(key): value = None diff = get_diff_of_htmls(self.diff[key][0] or "", self.diff[key][1] or "") if diff: key = "{}_diff".format(key) value = (None, diff) return (key, value) def resolve_value(field, key): data = self.values[field] key = str(key) if key not in data: return None return data[key] for key in self.diff: value = None if key in IGNORE_DIFF_FIELDS: continue elif key in ["description", "content", "blocked_note"]: (key, value) = resolve_diff_value(key) elif key in users_keys: value = [resolve_value("users", x) for x in self.diff[key]] elif key == "points": points = {} pointsold = self.diff["points"][0] pointsnew = self.diff["points"][1] # pointsold = pointsnew if pointsold is None: for role_id, point_id in pointsnew.items(): role_name = resolve_value("roles", role_id) points[role_name] = [ None, resolve_value("points", point_id) ] else: for role_id, point_id in pointsnew.items(): role_name = resolve_value("roles", role_id) oldpoint_id = pointsold.get(role_id, None) points[role_name] = [ resolve_value("points", oldpoint_id), resolve_value("points", point_id) ] # Process that removes points entries with # duplicate value. for role in dict(points): values = points[role] if values[1] == values[0]: del points[role] if points: value = points elif key == "attachments": attachments = { "new": [], "changed": [], "deleted": [], } oldattachs = {x["id"]: x for x in self.diff["attachments"][0]} newattachs = {x["id"]: x for x in self.diff["attachments"][1]} for aid in set( tuple(oldattachs.keys()) + tuple(newattachs.keys())): if aid in oldattachs and aid in newattachs: changes = make_diff_from_dicts( oldattachs[aid], newattachs[aid], excluded_keys=("filename", "url", "thumb_url")) if changes: change = { "filename": newattachs.get(aid, {}).get("filename", ""), "url": newattachs.get(aid, {}).get("url", ""), "thumb_url": newattachs.get(aid, {}).get("thumb_url", ""), "changes": changes } attachments["changed"].append(change) elif aid in oldattachs and aid not in newattachs: attachments["deleted"].append(oldattachs[aid]) elif aid not in oldattachs and aid in newattachs: attachments["new"].append(newattachs[aid]) if attachments["new"] or attachments["changed"] or attachments[ "deleted"]: value = attachments elif key == "custom_attributes": custom_attributes = { "new": [], "changed": [], "deleted": [], } oldcustattrs = { x["id"]: x for x in self.diff["custom_attributes"][0] or [] } newcustattrs = { x["id"]: x for x in self.diff["custom_attributes"][1] or [] } for aid in set( tuple(oldcustattrs.keys()) + tuple(newcustattrs.keys())): if aid in oldcustattrs and aid in newcustattrs: changes = make_diff_from_dicts(oldcustattrs[aid], newcustattrs[aid], excluded_keys=("name")) newcustattr = newcustattrs.get(aid, {}) if changes: change_type = newcustattr.get("type", TEXT_TYPE) old_value = oldcustattrs[aid].get("value", "") new_value = newcustattrs[aid].get("value", "") value_diff = get_diff_of_htmls( old_value, new_value) change = { "name": newcustattr.get("name", ""), "changes": changes, "type": change_type, "value_diff": value_diff } custom_attributes["changed"].append(change) elif aid in oldcustattrs and aid not in newcustattrs: custom_attributes["deleted"].append(oldcustattrs[aid]) elif aid not in oldcustattrs and aid in newcustattrs: new_value = newcustattrs[aid].get("value", "") value_diff = get_diff_of_htmls("", new_value) newcustattrs[aid]["value_diff"] = value_diff custom_attributes["new"].append(newcustattrs[aid]) if custom_attributes["new"] or custom_attributes[ "changed"] or custom_attributes["deleted"]: value = custom_attributes elif key == "user_stories": user_stories = { "new": [], "deleted": [], } olduss = {x["id"]: x for x in self.diff["user_stories"][0]} newuss = {x["id"]: x for x in self.diff["user_stories"][1]} for usid in set(tuple(olduss.keys()) + tuple(newuss.keys())): if usid in olduss and usid not in newuss: user_stories["deleted"].append(olduss[usid]) elif usid not in olduss and usid in newuss: user_stories["new"].append(newuss[usid]) if user_stories["new"] or user_stories["deleted"]: value = user_stories elif key in self.values: value = [resolve_value(key, x) for x in self.diff[key]] else: value = self.diff[key] if not value: continue result[key] = value self.values_diff_cache = result # Update values_diff_cache without dispatching signals HistoryEntry.objects.filter(pk=self.pk).update( values_diff_cache=self.values_diff_cache) return self.values_diff_cache class Meta: ordering = ["created_at"]
class User(AbstractBaseUser, PermissionsMixin): uuid = models.CharField(max_length=32, editable=False, null=False, blank=False, unique=True, default=get_default_uuid) username = models.CharField(_("username"), max_length=255, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and " "/./-/_ characters"), validators=[ validators.RegexValidator(re.compile("^[\w.-]+$"), _("Enter a valid username."), "invalid") ]) email = models.EmailField(_("email address"), max_length=255, blank=True, unique=True) is_active = models.BooleanField(_("active"), default=True, help_text=_("Designates whether this user should be treated as " "active. Unselect this instead of deleting accounts.")) full_name = models.CharField(_("full name"), max_length=256, blank=True) color = models.CharField(max_length=9, null=False, blank=True, default=generate_random_hex_color, verbose_name=_("color")) bio = models.TextField(null=False, blank=True, default="", verbose_name=_("biography")) photo = models.FileField(upload_to=get_user_file_path, max_length=500, null=True, blank=True, verbose_name=_("photo")) date_joined = models.DateTimeField(_("date joined"), default=timezone.now) accepted_terms = models.BooleanField(_("accepted terms"), default=True) read_new_terms = models.BooleanField(_("new terms read"), default=False) lang = models.CharField(max_length=20, null=True, blank=True, default="", verbose_name=_("default language")) theme = models.CharField(max_length=100, null=True, blank=True, default="", verbose_name=_("default theme")) timezone = models.CharField(max_length=20, null=True, blank=True, default="", verbose_name=_("default timezone")) colorize_tags = models.BooleanField(null=False, blank=True, default=False, verbose_name=_("colorize tags")) token = models.CharField(max_length=200, null=True, blank=True, default=None, verbose_name=_("token")) email_token = models.CharField(max_length=200, null=True, blank=True, default=None, verbose_name=_("email token")) new_email = models.EmailField(_("new email address"), null=True, blank=True) is_system = models.BooleanField(null=False, blank=False, default=False) max_private_projects = models.IntegerField(null=True, blank=True, default=settings.MAX_PRIVATE_PROJECTS_PER_USER, verbose_name=_("max number of owned private projects")) max_public_projects = models.IntegerField(null=True, blank=True, default=settings.MAX_PUBLIC_PROJECTS_PER_USER, verbose_name=_("max number of owned public projects")) max_memberships_private_projects = models.IntegerField(null=True, blank=True, default=settings.MAX_MEMBERSHIPS_PRIVATE_PROJECTS, verbose_name=_("max number of memberships for " "each owned private project")) max_memberships_public_projects = models.IntegerField(null=True, blank=True, default=settings.MAX_MEMBERSHIPS_PUBLIC_PROJECTS, verbose_name=_("max number of memberships for " "each owned public project")) projects_activity = JSONField(null=True, blank=True, default=None) _cached_memberships = None _cached_liked_ids = None _cached_watched_ids = None _cached_notify_levels = None USERNAME_FIELD = "username" REQUIRED_FIELDS = ["email"] objects = UserManager() class Meta: verbose_name = "user" verbose_name_plural = "users" ordering = ["username"] def __str__(self): return self.get_full_name() def _fill_cached_memberships(self): self._cached_memberships = {} qs = self.memberships.select_related("user", "project", "role") for membership in qs.all(): self._cached_memberships[membership.project.id] = membership @property def cached_memberships(self): if self._cached_memberships is None: self._fill_cached_memberships() return self._cached_memberships.values() def cached_membership_for_project(self, project): if self._cached_memberships is None: self._fill_cached_memberships() return self._cached_memberships.get(project.id, None) def is_fan(self, obj): if self._cached_liked_ids is None: self._cached_liked_ids = set() for like in self.likes.select_related("content_type").all(): like_id = "{}-{}".format(like.content_type.id, like.object_id) self._cached_liked_ids.add(like_id) obj_type = ContentType.objects.get_for_model(obj) obj_id = "{}-{}".format(obj_type.id, obj.id) return obj_id in self._cached_liked_ids def is_watcher(self, obj): if self._cached_watched_ids is None: self._cached_watched_ids = set() for watched in self.watched.select_related("content_type").all(): watched_id = "{}-{}".format(watched.content_type.id, watched.object_id) self._cached_watched_ids.add(watched_id) notify_policies = self.notify_policies.select_related("project")\ .exclude(notify_level=NotifyLevel.none) for notify_policy in notify_policies: obj_type = ContentType.objects.get_for_model(notify_policy.project) watched_id = "{}-{}".format(obj_type.id, notify_policy.project.id) self._cached_watched_ids.add(watched_id) obj_type = ContentType.objects.get_for_model(obj) obj_id = "{}-{}".format(obj_type.id, obj.id) return obj_id in self._cached_watched_ids def get_notify_level(self, project): if self._cached_notify_levels is None: self._cached_notify_levels = {} for notify_policy in self.notify_policies.select_related("project"): self._cached_notify_levels[notify_policy.project.id] = notify_policy.notify_level return self._cached_notify_levels.get(project.id, None) def get_short_name(self): "Returns the short name for the user." return self.username def get_full_name(self): return self.full_name or self.username or self.email def contacts_visible_by_user(self, user): qs = User.objects.filter(is_active=True) project_ids = services.get_visible_project_ids(self, user) qs = qs.filter(memberships__project_id__in=project_ids) qs = qs.exclude(id=self.id) return qs def save(self, *args, **kwargs): get_token_for_user(self, "cancel_account") super().save(*args, **kwargs) def cancel(self): with advisory_lock("delete-user"): deleted_user_prefix = "deleted-user-{}".format(timestamp_ms()) self.username = slugify_uniquely(deleted_user_prefix, User, slugfield="username") self.email = "{}@taiga.io".format(self.username) self.is_active = False self.full_name = "Deleted user" self.color = "" self.bio = "" self.lang = "" self.theme = "" self.timezone = "" self.colorize_tags = True self.token = None self.set_unusable_password() self.photo = None self.save() self.auth_data.all().delete() # Blocking all owned projects self.owned_projects.update(blocked_code=BLOCKED_BY_OWNER_LEAVING) # Remove all memberships self.memberships.all().delete()
class UserStory(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.Model): ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None, verbose_name=_("ref")) milestone = models.ForeignKey("milestones.Milestone", null=True, blank=True, default=None, related_name="user_stories", on_delete=models.SET_NULL, verbose_name=_("milestone")) project = models.ForeignKey("projects.Project", null=False, blank=False, related_name="user_stories", verbose_name=_("project")) owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, related_name="owned_user_stories", verbose_name=_("owner"), on_delete=models.SET_NULL) status = models.ForeignKey("projects.UserStoryStatus", null=True, blank=True, related_name="user_stories", verbose_name=_("status"), on_delete=models.SET_NULL) is_closed = models.BooleanField(default=False) points = models.ManyToManyField("projects.Points", blank=False, related_name="userstories", through="RolePoints", verbose_name=_("points")) backlog_order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms, verbose_name=_("backlog order")) sprint_order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms, verbose_name=_("sprint order")) kanban_order = models.BigIntegerField(null=False, blank=False, default=timestamp_ms, verbose_name=_("kanban order")) created_date = models.DateTimeField(null=False, blank=False, verbose_name=_("created date"), default=timezone.now) modified_date = models.DateTimeField(null=False, blank=False, verbose_name=_("modified date")) finish_date = models.DateTimeField(null=True, blank=True, verbose_name=_("finish date")) subject = models.TextField(null=False, blank=False, verbose_name=_("subject")) description = models.TextField(null=False, blank=True, verbose_name=_("description")) assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True, default=None, related_name="userstories_assigned_to_me", verbose_name=_("assigned to")) client_requirement = models.BooleanField(default=False, null=False, blank=True, verbose_name=_("is client requirement")) team_requirement = models.BooleanField(default=False, null=False, blank=True, verbose_name=_("is team requirement")) attachments = GenericRelation("attachments.Attachment") generated_from_issue = models.ForeignKey("issues.Issue", null=True, blank=True, on_delete=models.SET_NULL, related_name="generated_user_stories", verbose_name=_("generated from issue")) external_reference = ArrayField(models.TextField(null=False, blank=False), null=True, blank=True, default=None, verbose_name=_("external reference")) tribe_gig = PickledObjectField(null=True, blank=True, default=None, verbose_name="taiga tribe gig") publish_date = models.TextField(null=True, blank=True, verbose_name=_("publish date")) publish_time = models.TextField(null=True, blank=True, verbose_name=_("publish time")) social_media_attributes = JSONField(null=True, blank=True, verbose_name=_("social media attributes")) _importing = None class Meta: verbose_name = "user story" verbose_name_plural = "user stories" ordering = ["project", "backlog_order", "ref"] def save(self, *args, **kwargs): if not self._importing or not self.modified_date: self.modified_date = timezone.now() if not self.status: self.status = self.project.default_us_status super().save(*args, **kwargs) if not self.role_points.all(): for role in self.project.roles.all(): RolePoints.objects.create(role=role, points=self.project.default_points, user_story=self) def __str__(self): return "({1}) {0}".format(self.ref, self.subject) def __repr__(self): return "<UserStory %s>" % (self.id) def get_role_points(self): return self.role_points def get_total_points(self): not_null_role_points = [ rp.points.value for rp in self.role_points.all() if rp.points.value is not None ] #If we only have None values the sum should be None if not not_null_role_points: return None return sum(not_null_role_points)