Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions changelog.d/3572.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for setting shared dashboards as account default
44 changes: 43 additions & 1 deletion python/nav/models/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,26 @@ def unlocked_password(self):
def get_email_addresses(self):
return self.alert_addresses.filter(type__name=AlertSender.EMAIL)

@property
def has_default_dashboard(self):
"""Returns True if the user has a default dashboard preference set."""
return AccountDefaultDashboard.objects.filter(account_id=self.id).exists()

@property
def default_dashboard(self):
"""Returns the user's default dashboard, or None if not set."""
try:
mapping = AccountDefaultDashboard.objects.get(account_id=self.id)
return mapping.dashboard
except AccountDefaultDashboard.DoesNotExist:
return None

def set_default_dashboard(self, dashboard_id: int):
"""Sets the user's default dashboard preference."""
AccountDefaultDashboard.objects.update_or_create(
account_id=self.id, defaults={'dashboard_id': dashboard_id}
)
Comment on lines +373 to +375
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this is beautifully done!



class AccountGroup(models.Model):
"""NAV account groups"""
Expand Down Expand Up @@ -1640,7 +1660,6 @@ class AccountDashboard(models.Model):
"""Stores dashboards for each user"""

name = VarcharField()
is_default = models.BooleanField(default=False)
num_columns = models.IntegerField(default=3)
account = models.ForeignKey(
Account,
Expand Down Expand Up @@ -1683,11 +1702,34 @@ def can_edit(self, account):
def is_subscribed(self, account):
return self.subscribers.filter(account=account).exists()

def is_default_for_account(self, account):
default = account.default_dashboard
return default and default.id == self.id

class Meta(object):
db_table = 'account_dashboard'
ordering = ('name',)


class AccountDefaultDashboard(models.Model):
account = models.OneToOneField(
Account,
on_delete=models.CASCADE,
db_column='account_id',
primary_key=True,
related_name='default_dashboard_mapping',
)
dashboard = models.ForeignKey(
AccountDashboard,
on_delete=models.CASCADE,
db_column='dashboard_id',
related_name='default_for_accounts',
)

class Meta:
db_table = 'account_default_dashboard'


class AccountDashboardSubscription(models.Model):
"""Subscriptions for dashboards shared between users"""

Expand Down
11 changes: 11 additions & 0 deletions python/nav/models/sql/changes/sc.05.15.0002.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CREATE TABLE profiles.account_default_dashboard (
account_id INT PRIMARY KEY REFERENCES profiles.account(id) ON UPDATE CASCADE ON DELETE CASCADE,
dashboard_id INT NOT NULL REFERENCES profiles.account_dashboard(id) ON UPDATE CASCADE ON DELETE CASCADE
);

INSERT INTO profiles.account_default_dashboard (account_id, dashboard_id)
SELECT d.account_id, d.id
FROM profiles.account_dashboard AS d
WHERE d.is_default = true
ON CONFLICT (account_id) DO UPDATE
SET dashboard_id = EXCLUDED.dashboard_id;
33 changes: 33 additions & 0 deletions python/nav/models/sql/changes/sc.05.15.0003.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
-- Create a new dashboard, copy all the widgets from the default user to
-- the dashboard, and set the new dashboard as the default dashboard
-- for the newly created account.
CREATE OR REPLACE FUNCTION create_new_dashboard() RETURNS trigger AS $$
DECLARE
new_dashboard_id INTEGER;
BEGIN
-- Insert dashboard
INSERT INTO profiles.account_dashboard (account_id, is_default, num_columns)
VALUES (NEW.id, TRUE, 3)
RETURNING id INTO new_dashboard_id;

-- Copy navlets from default user
INSERT INTO profiles.account_navlet (account, navlet, displayorder, col, preferences, dashboard_id)
SELECT NEW.id, navlet, displayorder, col, preferences, new_dashboard_id
FROM profiles.account_navlet WHERE account=0;

-- Insert into account_default_dashboard
INSERT INTO profiles.account_default_dashboard (account_id, dashboard_id)
VALUES (NEW.id, new_dashboard_id);

RETURN NULL;
END
$$ LANGUAGE plpgsql;

-- Drop the trigger to allow re-creation with updated create_new_dashboard function
DROP TRIGGER IF EXISTS add_default_dashboard_on_account_create ON profiles.account;

-- Create the trigger
CREATE TRIGGER add_default_dashboard_on_account_create
AFTER INSERT ON profiles.account
FOR EACH ROW
EXECUTE PROCEDURE create_new_dashboard();
2 changes: 1 addition & 1 deletion python/nav/web/navlets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ def add_navlet(account, navlet, preferences=None, dashboard=None):
if preferences is None:
preferences = {}
if dashboard is None:
dashboard = AccountDashboard.objects.get(account=account, is_default=True)
dashboard = account.default_dashboard

accountnavlet = AccountNavlet(account=account, navlet=navlet, dashboard=dashboard)
accountnavlet.column, accountnavlet.order = find_new_placement()
Expand Down
5 changes: 4 additions & 1 deletion python/nav/web/sass/nav/custom.scss
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,12 @@
.widget-action {
border-bottom: none;
}
.popover-content {
.settings-popover {
width: 800px;
}
.shared-settings-popover {
width: 600px;
}
.row {
margin-bottom: 1em;
&:last-child {
Expand Down
2 changes: 1 addition & 1 deletion python/nav/web/templates/webfront/_dashboard_nav.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<span>
{{ d.name }}
</span>
<i class="fa fa-star {% if not d.is_default or d.shared_by_other %}hidden{% endif %}"
<i class="fa fa-star {% if not d.is_default %}hidden{% endif %}"
title="This is the default dashboard &mdash; it will be loaded when you log in. To change default, change to another dashboard and set it as default in the dashboard settings."></i>
<i class="fa fa-share-alt {% if not d.shared_by_other %}hidden{% endif %}"
title="This dashboard is shared by another account."></i>
Expand Down
18 changes: 12 additions & 6 deletions python/nav/web/templates/webfront/_dashboard_settings_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
>
<i class="fa fa-gear fa-lg"></i>
</div>
<div class="popover-content large">
<div class="popover-content large {% if can_edit %}settings-popover{% else %}shared-settings-popover{% endif %}">
{% if can_edit %}
<form id="form-rename-dashboard"
method="post"
data-dashboard="{{ dashboard.pk }}"
Expand All @@ -17,10 +18,13 @@
value="{{ dashboard.name }}">
<input type="submit" class="small button" value="Rename dashboard">
</form>
{% else %}
<p aria-label="Dashboard name">{{ dashboard.name }}</p>
{% endif %}

<div class="row">

<div class="column medium-4">
<div class="column medium-6">
<div id="default-dashboard-container"
data-is-default-dashboard="{{ dashboard.is_default|yesno:'1,0' }}">
<div class="alert-box">This is the default dashboard</div>
Expand All @@ -32,8 +36,7 @@
</form>
</div>
</div>

<div class="column medium-4">
<div class="column medium-6">
<a href="{% url 'export-dashboard' dashboard.pk %}" class="small button expand">Export dashboard
</a><br/>
<div class="nav-tooltip">
Expand All @@ -44,7 +47,8 @@
</div>
</div>

<div class="column medium-4">
{% if can_edit %}
<div class="column medium-6">
{% if request.account.account_dashboards.count > 1 and not dashboard.is_default %}
<form id="form-delete-dashboard" method="post" action="{% url 'delete-dashboard' dashboard.pk %}">
{% csrf_token %}
Expand All @@ -53,8 +57,9 @@
{% endif %}

</div>

{% endif %}
</div>
{% if can_edit %}
<div class="row">
<div class="column medium-4">
<h5>Columns</h5>
Expand All @@ -72,6 +77,7 @@ <h5>Sharing</h5>
</div>
<div class="column medium-4"></div>
</div>
{% endif %}
<div id="dashboard-settings-feedback">
</div>
</div>
Expand Down
19 changes: 1 addition & 18 deletions python/nav/web/templates/webfront/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,24 +64,7 @@
title="Use compact layout for dashboard">
<i class="fa fa-compress"></i>
</div>

{% if can_edit %}
{% include 'webfront/_dashboard_settings_form.html' %}
{% else %}
<div class="nav-tooltip export-action">
<a
class="widget-action"
href="{% url 'export-dashboard' dashboard.pk %}"
aria-describedby="#export-action-tooltip"
>
<i class="fa fa-download" title="Export dashboard"></i>
</a>
<div id="export-action-tooltip" role="tooltip">
Download this dashboard's definition into a file that can later be imported by pressing the + next to the tab list
</div>
</div>
{% endif %}

{% include 'webfront/_dashboard_settings_form.html' %}
</div>

{# Buttons for selecting dashboards #}
Expand Down
59 changes: 44 additions & 15 deletions python/nav/web/webfront/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,51 @@ def find_dashboard(account, dashboard_id=None):
Either find a specific one or the default one. If none of those exist we
find the one with the most widgets.
"""
kwargs = {'pk': dashboard_id} if dashboard_id else {'is_default': True}
dashboard = (
_find_dashboard_by_id(account, dashboard_id)
if dashboard_id
else _find_default_dashboard(account)
)
dashboard.shared_by_other = dashboard.is_shared and dashboard.account != account
dashboard.is_default = dashboard.is_default_for_account(account)

return dashboard


def _find_dashboard_by_id(account, dashboard_id):
"""Find a specific dashboard by ID for this account"""
try:
dashboard = AccountDashboard.objects.get(
(Q(account=account) | Q(is_shared=True)), **kwargs
(Q(account=account) | Q(is_shared=True)), pk=dashboard_id
)
dashboard.shared_by_other = dashboard.is_shared and dashboard.account != account
return dashboard

except AccountDashboard.DoesNotExist:
if dashboard_id:
raise Http404
raise Http404

# Do we have a dashboard at all?
dashboards = AccountDashboard.objects.filter(account=account)
if dashboards.count() == 0:
raise Http404

# No default dashboard? Find the one with the most widgets
dashboard = dashboards.annotate(Count('widgets')).order_by('-widgets__count')[0]
except AccountDashboard.MultipleObjectsReturned:
# Grab the first one
dashboard = AccountDashboard.objects.filter(account=account, **kwargs)[0]
def _find_default_dashboard(account):
"""Find the default dashboard for this account"""
dashboard_id = (
account.default_dashboard.pk if account.has_default_dashboard else None
)

if dashboard_id:
dashboard = AccountDashboard.objects.filter(
Q(account=account) | Q(is_shared=True), pk=dashboard_id
).first()
if dashboard:
return dashboard
Comment on lines +51 to +60
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
dashboard_id = (
account.default_dashboard.pk if account.has_default_dashboard else None
)
if dashboard_id:
dashboard = AccountDashboard.objects.filter(
Q(account=account) | Q(is_shared=True), pk=dashboard_id
).first()
if dashboard:
return dashboard
if account.has_default_dashboard:
return account.default_dashboard


# No default dashboard? Find the one with the most widgets
dashboards = AccountDashboard.objects.filter(account=account)
if dashboards.count() == 0:
raise Http404
dashboard = (
dashboards.annotate(widget_count=Count('widgets'))
.order_by('-widget_count')
.first()
)

return dashboard

Expand All @@ -53,15 +77,20 @@ def get_dashboards_for_account(account) -> list[AccountDashboard]:
Returns a queryset of dashboards for the given account,
including those the account subscribes to.
"""
default_dashboard = account.default_dashboard
default_dashboard_id = default_dashboard.id if default_dashboard else None
dashboards = (
AccountDashboard.objects.filter(
Q(account=account) | Q(subscribers__account=account)
Q(account=account)
| Q(subscribers__account=account)
| Q(pk=default_dashboard_id)
Comment on lines +80 to +86
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To answer your question if setting a dashboard as default should also subscribe you to it, I would expect that to be the case. Because I would be surprised that if I set a dashboard as default and then make something else my default if that previous default dashboard would completely disappear from my feed.

Which if we decide for this would make this change superfluous.

)
.select_related('account')
.distinct()
)
for dash in dashboards:
dash.can_edit = dash.can_edit(account)
dash.shared_by_other = dash.is_shared and dash.account_id != account.id
dash.is_default = dash.id == default_dashboard_id

return list(dashboards)
21 changes: 7 additions & 14 deletions python/nav/web/webfront/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from nav.models.profiles import (
AccountDashboard,
AccountDashboardSubscription,
AccountDefaultDashboard,
AccountNavlet,
NavbarLink,
)
Expand Down Expand Up @@ -119,6 +120,9 @@ def toggle_dashboard_shared(request, did):

if not is_shared:
AccountDashboardSubscription.objects.filter(dashboard=dashboard).delete()
AccountDefaultDashboard.objects.exclude(account=account).filter(
dashboard=dashboard
).delete()

return _render_share_form_response(
request,
Expand Down Expand Up @@ -532,19 +536,8 @@ def set_account_preference(request):
def set_default_dashboard(request, did):
"""Set the default dashboard for the user"""
account = get_account(request)
dash = get_object_or_404(AccountDashboard, pk=did, account=account)

old_defaults = list(
AccountDashboard.objects.filter(account=account, is_default=True)
)
for old_default in old_defaults:
old_default.is_default = False

dash.is_default = True

AccountDashboard.objects.bulk_update(
objs=old_defaults + [dash], fields=["is_default"]
)
dash = find_dashboard(account, did)
account.set_default_dashboard(dash.id)

return HttpResponse('Default dashboard set to «{}»'.format(dash.name))

Expand All @@ -569,7 +562,7 @@ def delete_dashboard(request, did):

dash = get_object_or_404(AccountDashboard, pk=did, account=account)

if dash.is_default:
if dash.is_default_for_account(request.account):
return HttpResponseBadRequest('Cannot delete default dashboard')

dash.delete()
Expand Down
8 changes: 2 additions & 6 deletions tests/integration/web/navlets_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,7 @@ def _get_dashboard_url(dashboard: AccountDashboard):

@pytest.fixture
def dashboard(db, admin_account):
dashboard = AccountDashboard(
account=admin_account, name='Test Dashboard', is_default=True
)
dashboard = AccountDashboard(account=admin_account, name='Test Dashboard')
dashboard.save()
yield dashboard
dashboard.delete()
Expand All @@ -125,9 +123,7 @@ def other_account_dashboard(db):
password='apasswordthatislongenough123',
)
account.save()
dashboard = AccountDashboard(
account=account, name='Other Dashboard', is_default=True
)
dashboard = AccountDashboard(account=account, name='Other Dashboard')
dashboard.save()
yield dashboard
account.delete()
Expand Down
Loading
Loading