Skip to content

Commit

Permalink
Merge branch 'main' into rel
Browse files Browse the repository at this point in the history
  • Loading branch information
davidfischer committed Nov 21, 2024
2 parents 3b63ec1 + 3f40c12 commit 2259bc9
Show file tree
Hide file tree
Showing 38 changed files with 1,283 additions and 179 deletions.
21 changes: 21 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,27 @@ CHANGELOG
.. This is included by docs/developer/changelog.rst
Version v5.10.0
---------------

This release added a few advertiser features including
role based user accounts (for publishers too),
some visual cues, and bulk ad creation.
Other changes were mostly minor fixes, dependencies, and documentation.

:Date: November 20, 2024

* @dependabot[bot]: Bump aiohttp from 3.10.10 to 3.10.11 in /requirements (#943)
* @davidfischer: Add roles for advertisers and publishers (#941)
* @davidfischer: Add a visual cue for renewing flights on listview (#940)
* @davidfischer: Fix a celerybeat task configuration (#938)
* @mithucste30: Unknown task (#937)
* @davidfischer: Bulk ad upload (#935)
* @davidfischer: Remove explicit docs ad placement (#934)
* @ericholscher: Fix custom.css (#933)
* @davidfischer: Analyzer versions hotfix (#931)


Version v5.9.0
--------------

Expand Down
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"
Loading

0 comments on commit 2259bc9

Please sign in to comment.