@@ -1840,6 +1840,172 @@ def delete(self, *args, **kwargs):
18401840 self .secret_token .delete ()
18411841
18421842
1843+ class Organization (models .Model ):
1844+ """
1845+ Represents an organization that manages and owns channels.
1846+ Organizations can have roles defined for users and manage multiple channels.
1847+ """
1848+
1849+ id = UUIDField (primary_key = True , default = uuid .uuid4 )
1850+ name = models .CharField (max_length = 200 , db_index = True )
1851+ description = models .TextField (blank = True )
1852+ tagline = models .CharField (
1853+ max_length = 150 , blank = True , null = True , help_text = "Short description"
1854+ )
1855+ website = models .URLField (max_length = 200 , blank = True , null = True )
1856+ email = models .EmailField (blank = True , null = True )
1857+ thumbnail = models .TextField (blank = True , null = True )
1858+ thumbnail_encoding = JSONField (default = dict )
1859+ public = models .BooleanField (
1860+ default = False , db_index = True , help_text = "Whether organization is publicly visible"
1861+ )
1862+ deleted = models .BooleanField (
1863+ default = False , db_index = True , help_text = "Soft delete flag"
1864+ )
1865+
1866+ # Foreign Keys
1867+ channel = models .ForeignKey (
1868+ "Channel" ,
1869+ null = True ,
1870+ blank = True ,
1871+ related_name = "organization_channel" ,
1872+ on_delete = models .SET_NULL ,
1873+ help_text = "Primary channel managed by this organization" ,
1874+ )
1875+ organization_role = models .ForeignKey (
1876+ "OrganizationRole" ,
1877+ null = True ,
1878+ blank = True ,
1879+ related_name = "organizations" ,
1880+ on_delete = models .SET_NULL ,
1881+ help_text = "Tracking the creator or primary role associated with this organization" ,
1882+ )
1883+
1884+ # Metadata
1885+ preferences = models .TextField (
1886+ default = DEFAULT_USER_PREFERENCES , help_text = "Organization preferences as JSON"
1887+ )
1888+ settings = JSONField (default = dict , help_text = "Additional organization settings" )
1889+ created_at = models .DateTimeField (auto_now_add = True )
1890+ updated_at = models .DateTimeField (auto_now = True )
1891+
1892+ objects = CustomManager ()
1893+
1894+ class Meta :
1895+ verbose_name = "Organization"
1896+ verbose_name_plural = "Organizations"
1897+ ordering = ["name" ]
1898+ indexes = [
1899+ models .Index (fields = ["name" ], name = "organization_name_idx" ),
1900+ models .Index (
1901+ fields = ["deleted" , "public" ], name = "organization_deleted_public_idx"
1902+ ),
1903+ models .Index (fields = ["created_at" ], name = "organization_created_at_idx" ),
1904+ ]
1905+
1906+ def __str__ (self ):
1907+ return self .name
1908+
1909+
1910+ class OrganizationRole (models .Model ):
1911+ """
1912+ Through model for the User-Organization relationship.
1913+ Defines the role and membership status of a user within an organization.
1914+ Each user-organization pairing has its own role and metadata.
1915+ """
1916+
1917+ STATUS_CHOICES = [
1918+ ("active" , "Active" ),
1919+ ("inactive" , "Inactive" ),
1920+ ("pending" , "Pending Invitation" ),
1921+ ("suspended" , "Suspended" ),
1922+ ("declined" , "Declined Invitation" ),
1923+ ]
1924+
1925+ id = UUIDField (primary_key = True , default = uuid .uuid4 )
1926+
1927+ # Foreign Keys
1928+ user = models .ForeignKey (
1929+ settings .AUTH_USER_MODEL ,
1930+ on_delete = models .CASCADE ,
1931+ related_name = "organization_roles" ,
1932+ help_text = "User in the organization" ,
1933+ )
1934+ organization = models .ForeignKey (
1935+ "Organization" ,
1936+ on_delete = models .CASCADE ,
1937+ related_name = "user_roles" ,
1938+ help_text = "Organization the user belongs to" ,
1939+ )
1940+
1941+ # Role and status fields
1942+ role = models .CharField (
1943+ max_length = 100 ,
1944+ help_text = "Role name (e.g., Admin, Editor, Viewer, Content Creator)" ,
1945+ )
1946+ description = models .TextField (
1947+ blank = True , help_text = "Description of the user's role within the organization"
1948+ )
1949+ permissions = JSONField (
1950+ default = dict , help_text = "JSON object defining user's permissions"
1951+ )
1952+ status = models .CharField (
1953+ max_length = 20 ,
1954+ choices = STATUS_CHOICES ,
1955+ default = "pending" ,
1956+ db_index = True ,
1957+ help_text = "Membership status" ,
1958+ )
1959+
1960+ # Metadata
1961+ joined_at = models .DateTimeField (
1962+ auto_now_add = True , help_text = "Date user joined the organization"
1963+ )
1964+ updated_at = models .DateTimeField (
1965+ auto_now = True , help_text = "Last update timestamp"
1966+ )
1967+ invitation_accepted_at = models .DateTimeField (
1968+ null = True , blank = True , help_text = "Date invitation was accepted"
1969+ )
1970+ invited_by = models .ForeignKey (
1971+ settings .AUTH_USER_MODEL ,
1972+ null = True ,
1973+ blank = True ,
1974+ related_name = "organization_invitations_sent" ,
1975+ on_delete = models .SET_NULL ,
1976+ help_text = "User who invited this user to the organization" ,
1977+ )
1978+
1979+ class Meta :
1980+ unique_together = ("user" , "organization" )
1981+ verbose_name = "Organization Role"
1982+ verbose_name_plural = "Organization Roles"
1983+ ordering = ["-joined_at" ]
1984+ indexes = [
1985+ models .Index (
1986+ fields = ["organization" , "status" ], name = "org_role_org_status_idx"
1987+ ),
1988+ models .Index (
1989+ fields = ["user" , "organization" ], name = "org_role_unique_idx"
1990+ ),
1991+ models .Index (fields = ["status" ], name = "org_role_status_idx" ),
1992+ ]
1993+
1994+ def __str__ (self ):
1995+ return f"{ self .user .email } - { self .organization .name } ({ self .role } )"
1996+
1997+ def accept_invitation (self ):
1998+ """Mark the invitation as accepted."""
1999+ self .status = "active"
2000+ self .invitation_accepted_at = timezone .now ()
2001+ self .save ()
2002+
2003+ def decline_invitation (self ):
2004+ """Mark the invitation as declined."""
2005+ self .status = "declined"
2006+ self .save ()
2007+
2008+
18432009class ContentTag (models .Model ):
18442010 id = UUIDField (primary_key = True , default = uuid .uuid4 )
18452011 tag_name = models .CharField (max_length = 50 )
0 commit comments