Skip to content

Commit

Permalink
Merge pull request #941 from readthedocs/davidfischer/roles-advertise…
Browse files Browse the repository at this point in the history
…rs-publishers

Add roles for advertisers and publishers
  • Loading branch information
davidfischer authored Nov 20, 2024
2 parents be79434 + b51a768 commit 2c8c38b
Show file tree
Hide file tree
Showing 18 changed files with 677 additions and 70 deletions.
19 changes: 17 additions & 2 deletions adserver/auth/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@
from simple_history.admin import SimpleHistoryAdmin

from .models import User
from .models import UserAdvertiserMember
from .models import UserPublisherMember


class UserAdvertiserInline(admin.TabularInline):
"""For inlining the user-advertiser relationship."""

model = UserAdvertiserMember
raw_id_fields = ("advertiser",)


class UserPublisherInline(admin.TabularInline):
"""For inlining the user-publisher relationship."""

model = UserPublisherMember
raw_id_fields = ("publisher",)


@admin.register(User)
Expand All @@ -19,8 +35,6 @@ class UserAdmin(SimpleHistoryAdmin):
_("Ad server details"),
{
"fields": (
"advertisers",
"publishers",
"flight_notifications",
"notify_on_completed_flights", # DEPRECATED
)
Expand All @@ -43,6 +57,7 @@ class UserAdmin(SimpleHistoryAdmin):
{"fields": ("last_login", "updated_date", "created_date")},
),
)
inlines = (UserAdvertiserInline, UserPublisherInline)
list_display = (
"email",
"name",
Expand Down
70 changes: 70 additions & 0 deletions adserver/auth/migrations/0009_user_advertiser_publisher_roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""
This migration was autocreated but has been customized.
See: https://docs.djangoproject.com/en/5.0/howto/writing-migrations/#changing-a-manytomanyfield-to-use-a-through-model
"""

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


class Migration(migrations.Migration):

dependencies = [
('adserver', '0098_rotation_aggregation'),
('adserver_auth', '0008_data_flight_notifications'),
]

operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.CreateModel(
name='UserAdvertiserMember',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# We add this in a separate operation below
# ('role', models.CharField(choices=[('Admin', 'Admin'), ('Manager', 'Manager'), ('Reporter', 'Reporter')], default='Admin', max_length=100)),
('advertiser', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='adserver.advertiser')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'adserver_auth_user_advertisers',
},
),
migrations.AlterField(
model_name='user',
name='advertisers',
field=models.ManyToManyField(blank=True, through='adserver_auth.UserAdvertiserMember', to='adserver.advertiser'),
),
migrations.CreateModel(
name='UserPublisherMember',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# We add this in a separate operation below
# ('role', models.CharField(choices=[('Admin', 'Admin'), ('Manager', 'Manager'), ('Reporter', 'Reporter')], default='Admin', max_length=100)),
('publisher', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='adserver.publisher')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'db_table': 'adserver_auth_user_publishers',
},
),
migrations.AlterField(
model_name='user',
name='publishers',
field=models.ManyToManyField(blank=True, through='adserver_auth.UserPublisherMember', to='adserver.publisher'),
),
],
),
migrations.AddField(
model_name="useradvertisermember",
name="role",
field=models.CharField(choices=[('Admin', 'Admin'), ('Manager', 'Manager'), ('Reporter', 'Reporter')], default='Admin', max_length=100),
),
migrations.AddField(
model_name="userpublishermember",
name="role",
field=models.CharField(choices=[('Admin', 'Admin'), ('Manager', 'Manager'), ('Reporter', 'Reporter')], default='Admin', max_length=100),
),
]
138 changes: 136 additions & 2 deletions adserver/auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,12 @@ class User(AbstractBaseUser, PermissionsMixin):
created_date = models.DateTimeField(_("create date"), auto_now_add=True)

# A user may have access to zero or more advertisers or publishers
advertisers = models.ManyToManyField(Advertiser, blank=True)
publishers = models.ManyToManyField(Publisher, blank=True)
advertisers = models.ManyToManyField(
Advertiser, blank=True, through="UserAdvertiserMember"
)
publishers = models.ManyToManyField(
Publisher, blank=True, through="UserPublisherMember"
)

# Notifications
flight_notifications = models.BooleanField(
Expand Down Expand Up @@ -115,6 +119,54 @@ def get_full_name(self):
def get_short_name(self):
return self.get_full_name()

def get_advertiser_role(self, advertiser):
"""
Returns the users role in this advertiser or None if the user has no permissions.
Staff status is not taken into account. Caches the result on the user so future calls
don't involve a DB lookup.
"""
if not hasattr(self, "_advertiser_roles"):
self._advertiser_roles = {}

if advertiser.pk in self._advertiser_roles:
return self._advertiser_roles[advertiser.pk]

membership = self.useradvertisermember_set.filter(
advertiser=advertiser,
).first()

role = None
if membership:
role = membership.role

self._advertiser_roles[advertiser.pk] = role
return role

def get_publisher_role(self, publisher):
"""
Returns the users role in this publisher or None if the user has no permissions.
Staff status is not taken into account. Caches the result on the user so future calls
don't involve a DB lookup.
"""
if not hasattr(self, "_publisher_roles"):
self._publisher_roles = {}

if publisher.pk in self._publisher_roles:
return self._publisher_roles[publisher.pk]

membership = self.userpublishermember_set.filter(
publisher=publisher,
).first()

role = None
if membership:
role = membership.role

self._publisher_roles[publisher.pk] = role
return role

def get_password_reset_url(self):
temp_key = default_token_generator.make_token(self)
path = reverse(
Expand All @@ -131,6 +183,36 @@ def get_password_reset_url(self):
scheme=scheme, domain=domain, path=path
)

def has_advertiser_permission(self, advertiser):
role = self.get_advertiser_role(advertiser)
return role is not None

def has_advertiser_manager_permission(self, advertiser):
role = self.get_advertiser_role(advertiser)
return role in (
UserAdvertiserMember.ROLE_ADMIN,
UserAdvertiserMember.ROLE_MANAGER,
)

def has_advertiser_admin_permission(self, advertiser):
role = self.get_advertiser_role(advertiser)
return role == UserAdvertiserMember.ROLE_ADMIN

def has_publisher_permission(self, publisher):
role = self.get_publisher_role(publisher)
return role is not None

def has_publisher_manager_permission(self, publisher):
role = self.get_publisher_role(publisher)
return role in (
UserPublisherMember.ROLE_ADMIN,
UserPublisherMember.ROLE_MANAGER,
)

def has_publisher_admin_permission(self, publisher):
role = self.get_publisher_role(publisher)
return role == UserPublisherMember.ROLE_ADMIN

def invite_user(self):
site = get_current_site(request=None)

Expand All @@ -146,3 +228,55 @@ def invite_user(self):
[self.email],
)
return True


class UserAdvertiserMember(models.Model):
"""User-Advertiser 'through' model."""

ROLE_ADMIN = "Admin"
ROLE_MANAGER = "Manager"
ROLE_REPORTER = "Reporter"
ROLES = (ROLE_ADMIN, ROLE_MANAGER, ROLE_REPORTER)

user = models.ForeignKey(User, on_delete=models.CASCADE)
advertiser = models.ForeignKey(Advertiser, on_delete=models.CASCADE)
role = models.CharField(
max_length=100,
choices=(
(ROLE_ADMIN, _(ROLE_ADMIN)),
(ROLE_MANAGER, _(ROLE_MANAGER)),
(ROLE_REPORTER, _(ROLE_REPORTER)),
),
default=ROLE_ADMIN,
)

class Meta:
# This was migrated from a regular many-to-many
# To do that, we needed to start with the same table
db_table = "adserver_auth_user_advertisers"


class UserPublisherMember(models.Model):
"""User-Publisher 'through' model."""

ROLE_ADMIN = "Admin"
ROLE_MANAGER = "Manager"
ROLE_REPORTER = "Reporter"
ROLES = (ROLE_ADMIN, ROLE_MANAGER, ROLE_REPORTER)

user = models.ForeignKey(User, on_delete=models.CASCADE)
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
role = models.CharField(
max_length=100,
choices=(
(ROLE_ADMIN, _(ROLE_ADMIN)),
(ROLE_MANAGER, _(ROLE_MANAGER)),
(ROLE_REPORTER, _(ROLE_REPORTER)),
),
default=ROLE_ADMIN,
)

class Meta:
# This was migrated from a regular many-to-many
# To do that, we needed to start with the same table
db_table = "adserver_auth_user_publishers"
10 changes: 10 additions & 0 deletions adserver/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _

from .auth.models import UserAdvertiserMember
from .models import Advertisement
from .models import Campaign
from .models import Flight
Expand Down Expand Up @@ -1467,6 +1468,14 @@ class InviteUserForm(forms.ModelForm):
without a duplicate being created.
"""

role = forms.ChoiceField(
required=True,
# This form works for both publishers and advertisers
# Currently, the roles are the same for both
# If that ever changes, this will need an update
choices=((r, r) for r in UserAdvertiserMember.ROLES),
)

def __init__(self, *args, **kwargs):
"""Add the form helper and customize the look of the form."""
super().__init__(*args, **kwargs)
Expand All @@ -1476,6 +1485,7 @@ def __init__(self, *args, **kwargs):
"",
Field("name"),
Field("email", placeholder="[email protected]"),
Field("role"),
css_class="my-3",
),
Submit("submit", "Send invite"),
Expand Down
Loading

0 comments on commit 2c8c38b

Please sign in to comment.