Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add org support - UX and roles #416

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b39f3f6
Add org support with role and UI view
nikochiko Jul 15, 2024
0e6334b
Add uncommitted orgs files
nikochiko Jul 15, 2024
cd86d05
Add support for accepting org domain name
nikochiko Jul 17, 2024
68ecb7f
Add delete icon
nikochiko Jul 17, 2024
695ddd6
Make modals rounded
nikochiko Jul 17, 2024
d8616c8
Add confirmation modals for removing members, move danger zone to edit
nikochiko Jul 17, 2024
e139684
make use of django-safedelete library
nikochiko Jul 18, 2024
136e086
Add django-safedelete to poetry
nikochiko Jul 18, 2024
b995c91
Move transfer ownership functionality to leave modal
nikochiko Jul 18, 2024
141fac5
soft_delete -> delete
nikochiko Jul 18, 2024
edc6a49
don't show invitation role in inline table
nikochiko Jul 18, 2024
9f981a4
python magic to reuse same form for create/edit org
nikochiko Jul 18, 2024
1c74a65
Add invite_id and other Org model+procedure changes
nikochiko Jul 23, 2024
dcba085
Add invitation page
nikochiko Jul 23, 2024
937c76c
Add signals to auto-delete orgs and auto-add members
nikochiko Jul 23, 2024
c96f7fb
add invitation page
nikochiko Jul 23, 2024
513acfd
Add email templates
nikochiko Jul 23, 2024
3c34625
Use UniqueConstraint instead of unique_together for membership
nikochiko Jul 23, 2024
3eee1da
Merge remote-tracking branch 'origin/master' into org-support
nikochiko Jul 23, 2024
32449b9
rename get_route_url -> get_app_route_url in orgs/
nikochiko Jul 23, 2024
9871594
Add orgs/tasks.py
nikochiko Jul 23, 2024
73cb176
Merge branch 'master' into org-support
nikochiko Aug 7, 2024
d7f265d
Remove gooey_ui dir
nikochiko Aug 7, 2024
209ed5d
gooey gui renaming
nikochiko Aug 7, 2024
cad189d
update poetry.lock
nikochiko Aug 8, 2024
e508a4e
procfile: use && instead of ; between cd and npm run
nikochiko Aug 8, 2024
066b7b5
rename st->gui in account.py
nikochiko Aug 8, 2024
4eb04d3
rename st -> gui
nikochiko Aug 12, 2024
0ebcc56
Merge branch 'master' into org-support
nikochiko Aug 19, 2024
d7092d4
Merge branch 'master' into org-support
nikochiko Aug 30, 2024
6e9620e
make org page only accessible to admins
nikochiko Aug 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Procfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ dashboard: poetry run streamlit run Home.py --server.port 8501 --server.headless

celery: poetry run celery -A celeryapp worker -P threads -c 16 -l DEBUG

ui: cd ../gooey-gui/; PORT=3000 npm run dev
ui: cd ../gooey-gui/ && env PORT=3000 npm run dev
7 changes: 7 additions & 0 deletions daras_ai_v2/icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,21 @@
camera = '<i class="fa-regular fa-camera"></i>'
cancel = '<i class="fa-regular fa-xmark-large"></i>'
edit = '<i class="fa-regular fa-pencil"></i>'
delete = '<i class="fa-solid fa-trash-can"></i>'
link = '<i class="fa-regular fa-link"></i>'
company = '<i class="fa-regular fa-buildings"></i>'
copy = '<i class="fa-solid fa-copy"></i>'
preview = '<i class="fa-solid fa-eye"></i>'
time = '<i class="fa-regular fa-clock"></i>'
email = '<i class="fa-solid fa-envelope"></i>'
add = '<i class="fa-regular fa-add"></i>'

code = '<i class="fa-regular fa-code"></i>'
chat = '<i class="fa-regular fa-messages"></i>'
admin = '<i class="fa-solid fa-shield-halved"></i>'
remove_user = '<i class="fa-solid fa-user-minus"></i>'
add_user = '<i class="fa-solid fa-user-plus"></i>'
transfer = '<i class="fa-solid fa-right-left"></i>'

# brands
github = '<i class="fa-brands fa-github"></i>'
Expand Down
7 changes: 7 additions & 0 deletions daras_ai_v2/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
# the order matters, since we want to override the admin templates
"django.forms", # needed to override admin forms
"django.contrib.admin",
"safedelete",
"app_users",
"files",
"url_shortener",
Expand All @@ -63,6 +64,7 @@
"handles",
"payments",
"functions",
"orgs",
]

MIDDLEWARE = [
Expand Down Expand Up @@ -394,6 +396,11 @@
DENO_FUNCTIONS_AUTH_TOKEN = config("DENO_FUNCTIONS_AUTH_TOKEN", "")
DENO_FUNCTIONS_URL = config("DENO_FUNCTIONS_URL", "")

ORG_INVITATION_EXPIRY_DAYS = config("ORG_INVITATIONS_EXPIRY_IN_DAYS", 10, cast=int)
ORG_INVITATION_EMAIL_COOLDOWN_INTERVAL = config(
"ORG_INVITATION_EMAIL_COOLDOWN_INTERVAL", 60 * 60 * 24, cast=int # 24 hours
)

TWILIO_ACCOUNT_SID = config("TWILIO_ACCOUNT_SID", "")
TWILIO_API_KEY_SID = config("TWILIO_API_KEY_SID", "")
TWILIO_API_KEY_SECRET = config("TWILIO_API_KEY_SECRET", "")
Empty file added orgs/__init__.py
Empty file.
104 changes: 104 additions & 0 deletions orgs/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from django.contrib import admin
from safedelete.admin import SafeDeleteAdmin, SafeDeleteAdminFilter

from bots.admin_links import change_obj_url
from orgs.models import Org, OrgMembership, OrgInvitation


class OrgMembershipInline(admin.TabularInline):
model = OrgMembership
extra = 0
show_change_link = True
fields = ["user", "role", "created_at", "updated_at"]
readonly_fields = ["created_at", "updated_at"]
ordering = ["-created_at"]
can_delete = False
show_change_link = True


class OrgInvitationInline(admin.TabularInline):
model = OrgInvitation
extra = 0
show_change_link = True
fields = [
"invitee_email",
"inviter",
"status",
"auto_accepted",
"created_at",
"updated_at",
]
readonly_fields = ["auto_accepted", "created_at", "updated_at"]
ordering = ["status", "-created_at"]
can_delete = False
show_change_link = True


@admin.register(Org)
class OrgAdmin(SafeDeleteAdmin):
list_display = [
"name",
"domain_name",
"created_at",
"updated_at",
] + list(SafeDeleteAdmin.list_display)
list_filter = [SafeDeleteAdminFilter] + list(SafeDeleteAdmin.list_filter)
fields = ["name", "domain_name", "created_by", "created_at", "updated_at"]
search_fields = ["name", "domain_name"]
readonly_fields = ["created_at", "updated_at"]
inlines = [OrgMembershipInline, OrgInvitationInline]
ordering = ["-created_at"]


@admin.register(OrgMembership)
class OrgMembershipAdmin(SafeDeleteAdmin):
list_display = [
"user",
"org",
"role",
"created_at",
"updated_at",
] + list(SafeDeleteAdmin.list_display)
list_filter = ["org", "role", SafeDeleteAdminFilter] + list(
SafeDeleteAdmin.list_filter
)

def get_readonly_fields(
self, request: "HttpRequest", obj: OrgMembership | None = None
) -> list[str]:
readonly_fields = list(super().get_readonly_fields(request, obj))
if obj and obj.org and obj.org.deleted:
return readonly_fields + ["deleted_org"]
else:
return readonly_fields

@admin.display
def deleted_org(self, obj):
org = Org.deleted_objects.get(pk=obj.org_id)
return change_obj_url(org)


@admin.register(OrgInvitation)
class OrgInvitationAdmin(SafeDeleteAdmin):
fields = [
"org",
"invitee_email",
"inviter",
"role",
"status",
"auto_accepted",
"created_at",
"updated_at",
]
list_display = [
"org",
"invitee_email",
"inviter",
"status",
"created_at",
"updated_at",
] + list(SafeDeleteAdmin.list_display)
list_filter = ["org", "inviter", "role", SafeDeleteAdminFilter] + list(
SafeDeleteAdmin.list_filter
)
readonly_fields = ["auto_accepted"]
11 changes: 11 additions & 0 deletions orgs/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.apps import AppConfig


class OrgsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "orgs"

def ready(self):
from . import signals

assert signals
81 changes: 81 additions & 0 deletions orgs/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Generated by Django 4.2.7 on 2024-07-18 15:41

from django.db import migrations, models
import django.db.models.deletion
import orgs.models


class Migration(migrations.Migration):

initial = True

dependencies = [
('app_users', '0019_alter_appusertransaction_reason'),
]

operations = [
migrations.CreateModel(
name='Org',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('deleted', models.DateTimeField(db_index=True, editable=False, null=True)),
('deleted_by_cascade', models.BooleanField(default=False, editable=False)),
('org_id', models.CharField(blank=True, max_length=100, null=True, unique=True)),
('name', models.CharField(max_length=100)),
('logo', models.URLField(blank=True, null=True)),
('domain_name', models.CharField(blank=True, max_length=30, null=True, validators=[orgs.models.validate_org_domain_name])),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='app_users.appuser')),
],
),
migrations.CreateModel(
name='OrgInvitation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('deleted', models.DateTimeField(db_index=True, editable=False, null=True)),
('deleted_by_cascade', models.BooleanField(default=False, editable=False)),
('invite_id', models.CharField(max_length=100, unique=True)),
('invitee_email', models.EmailField(max_length=254)),
('status', models.IntegerField(choices=[(1, 'Pending'), (2, 'Accepted'), (3, 'Rejected'), (4, 'Canceled'), (5, 'Expired')], default=1)),
('auto_accepted', models.BooleanField(default=False)),
('role', models.IntegerField(choices=[(1, 'Owner'), (2, 'Admin'), (3, 'Member')], default=3)),
('last_email_sent_at', models.DateTimeField(blank=True, default=False, null=True)),
('status_changed_at', models.DateTimeField(blank=True, default=False, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('inviter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_invitations', to='app_users.appuser')),
('org', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invitations', to='orgs.org')),
('status_changed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='app_users.appuser')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='OrgMembership',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('deleted', models.DateTimeField(db_index=True, editable=False, null=True)),
('deleted_by_cascade', models.BooleanField(default=False, editable=False)),
('role', models.IntegerField(choices=[(1, 'Owner'), (2, 'Admin'), (3, 'Member')], default=3)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('invitation', models.OneToOneField(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='membership', to='orgs.orginvitation')),
('org', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='orgs.org')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='org_memberships', to='app_users.appuser')),
],
options={
'unique_together': {('org', 'user', 'deleted')},
},
),
migrations.AddField(
model_name='org',
name='members',
field=models.ManyToManyField(related_name='orgs', through='orgs.OrgMembership', to='app_users.appuser'),
),
migrations.AlterUniqueTogether(
name='org',
unique_together={('domain_name', 'deleted')},
),
]
35 changes: 35 additions & 0 deletions orgs/migrations/0002_alter_org_unique_together_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 4.2.7 on 2024-07-22 14:45

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('orgs', '0001_initial'),
]

operations = [
migrations.AlterUniqueTogether(
name='org',
unique_together=set(),
),
migrations.AlterField(
model_name='orginvitation',
name='last_email_sent_at',
field=models.DateTimeField(blank=True, default=None, null=True),
),
migrations.AlterField(
model_name='orginvitation',
name='status_changed_at',
field=models.DateTimeField(blank=True, default=None, null=True),
),
migrations.AddConstraint(
model_name='org',
constraint=models.UniqueConstraint(condition=models.Q(('deleted__isnull', True)), fields=('domain_name',), name='unique_domain_name_when_not_deleted'),
),
migrations.RemoveField(
model_name='org',
name='members',
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Generated by Django 4.2.7 on 2024-07-23 11:45

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('app_users', '0019_alter_appusertransaction_reason'),
('orgs', '0002_alter_org_unique_together_and_more'),
]

operations = [
migrations.RemoveConstraint(
model_name='org',
name='unique_domain_name_when_not_deleted',
),
migrations.AlterUniqueTogether(
name='orgmembership',
unique_together=set(),
),
migrations.AlterField(
model_name='orginvitation',
name='status_changed_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='received_invitations', to='app_users.appuser'),
),
migrations.AddConstraint(
model_name='org',
constraint=models.UniqueConstraint(condition=models.Q(('deleted__isnull', True)), fields=('domain_name',), name='unique_domain_name_when_not_deleted', violation_error_message='This domain name is already in use by another team. Contact Gooey.AI Support <[email protected]> if you think this is a mistake.'),
),
migrations.AddConstraint(
model_name='orgmembership',
constraint=models.UniqueConstraint(condition=models.Q(('deleted__isnull', True)), fields=('org', 'user'), name='unique_org_user'),
),
]
Empty file added orgs/migrations/__init__.py
Empty file.
Loading
Loading