Browse Source

Menus cleanup (#1295)

* Menus cleanup first pass

* Cleanup menu items feature
Rafał Pitoń 5 years ago
parent
commit
3976858286

+ 2 - 3
devproject/settings.py

@@ -193,8 +193,8 @@ INSTALLED_APPS = [
     "misago.socialauth",
     "misago.graphql",
     "misago.faker",
-    "misago.sso",
     "misago.menus",
+    "misago.sso",
 ]
 
 INTERNAL_IPS = ["127.0.0.1"]
@@ -285,8 +285,7 @@ TEMPLATES = [
                 "misago.search.context_processors.search_providers",
                 "misago.themes.context_processors.theme",
                 "misago.legal.context_processors.legal_links",
-                "misago.menus.context_processors.navbar",
-                "misago.menus.context_processors.footer",
+                "misago.menus.context_processors.menus",
                 "misago.users.context_processors.user_links",
                 # Data preloaders
                 "misago.conf.context_processors.preload_settings_json",

+ 2 - 2
misago/conftest.py

@@ -6,6 +6,7 @@ from .categories.models import Category
 from .conf import SETTINGS_CACHE
 from .conf.dynamicsettings import DynamicSettings
 from .conf.staticsettings import StaticSettings
+from .menus import MENU_ITEMS_CACHE
 from .socialauth import SOCIALAUTH_CACHE
 from .test import MisagoClient
 from .themes import THEME_CACHE
@@ -13,7 +14,6 @@ from .threads.test import post_thread
 from .users import BANS_CACHE
 from .users.models import AnonymousUser
 from .users.test import create_test_superuser, create_test_user
-from .menus import MENU_LINKS_CACHE
 
 
 def get_cache_versions():
@@ -23,7 +23,7 @@ def get_cache_versions():
         SETTINGS_CACHE: "abcdefgh",
         SOCIALAUTH_CACHE: "abcdefgh",
         THEME_CACHE: "abcdefgh",
-        MENU_LINKS_CACHE: "abcdefgh",
+        MENU_ITEMS_CACHE: "abcdefgh",
     }
 
 

+ 1 - 1
misago/menus/__init__.py

@@ -1,3 +1,3 @@
 default_app_config = "misago.menus.apps.MisagoMenusConfig"
 
-MENU_LINKS_CACHE = "menus"
+MENU_ITEMS_CACHE = "menus"

+ 21 - 19
misago/menus/admin/__init__.py

@@ -2,34 +2,36 @@ from django.conf.urls import url
 from django.utils.translation import gettext_lazy as _
 
 from .views import (
-    DeleteMenuLink,
-    EditMenuLink,
-    MenuLinksList,
-    MoveDownMenuLink,
-    MoveUpMenuLink,
-    NewMenuLink,
+    DeleteMenuItem,
+    EditMenuItem,
+    MenuItemsList,
+    MoveDownMenuItem,
+    MoveUpMenuItem,
+    NewMenuItem,
 )
 
 
 class MisagoAdminExtension:
     def register_urlpatterns(self, urlpatterns):
-        # Menu links
-        urlpatterns.namespace(r"^links/", "links", "settings")
+        # Menu items
+        urlpatterns.namespace(r"^menu-items/", "menu-items", "settings")
         urlpatterns.patterns(
-            "settings:links",
-            url(r"^$", MenuLinksList.as_view(), name="index"),
-            url(r"^(?P<page>\d+)/$", MenuLinksList.as_view(), name="index"),
-            url(r"^new/$", NewMenuLink.as_view(), name="new"),
-            url(r"^edit/(?P<pk>\d+)/$", EditMenuLink.as_view(), name="edit"),
-            url(r"^delete/(?P<pk>\d+)/$", DeleteMenuLink.as_view(), name="delete"),
-            url(r"^down/(?P<pk>(\w|-)+)/$", MoveDownMenuLink.as_view(), name="down"),
-            url(r"^up/(?P<pk>(\w|-)+)/$", MoveUpMenuLink.as_view(), name="up"),
+            "settings:menu-items",
+            url(r"^$", MenuItemsList.as_view(), name="index"),
+            url(r"^(?P<page>\d+)/$", MenuItemsList.as_view(), name="index"),
+            url(r"^new/$", NewMenuItem.as_view(), name="new"),
+            url(r"^edit/(?P<pk>\d+)/$", EditMenuItem.as_view(), name="edit"),
+            url(r"^delete/(?P<pk>\d+)/$", DeleteMenuItem.as_view(), name="delete"),
+            url(r"^down/(?P<pk>(\w|-)+)/$", MoveDownMenuItem.as_view(), name="down"),
+            url(r"^up/(?P<pk>(\w|-)+)/$", MoveUpMenuItem.as_view(), name="up"),
         )
 
     def register_navigation_nodes(self, site):
         site.add_node(
-            name=_("Menu links"),
-            description=_("Add custom links to navbar and footer menus."),
+            name=_("Menu items"),
+            description=_(
+                "Use those options to add custom items to the navbar and footer menus."
+            ),
             parent="settings",
-            namespace="links",
+            namespace="menu-items",
         )

+ 24 - 26
misago/menus/admin/forms.py

@@ -1,49 +1,47 @@
 from django import forms
 from django.utils.translation import gettext_lazy as _
 
-from ..models import MenuLink
+from ...admin.forms import YesNoSwitch
+from ..models import MenuItem
 from ..cache import clear_menus_cache
 
 
-class MenuLinkForm(forms.ModelForm):
-    link = forms.URLField(
-        label=_("Link"),
-        help_text=_("URL where the link should point to."),
-        required=True,
+class MenuItemForm(forms.ModelForm):
+    title = forms.CharField(label=_("Title"))
+    url = forms.URLField(
+        label=_("URL"), help_text=_("URL where this item will point to.")
     )
-    title = forms.CharField(
-        label=_("Title"), help_text=_("Title that will be used"), required=True
-    )
-    position = forms.ChoiceField(
-        label=_("Position"),
-        choices=MenuLink.LINK_POSITION_CHOICES,
-        help_text=_("Position/s the link should be located"),
+    menu = forms.ChoiceField(
+        label=_("Menu"),
+        choices=MenuItem.MENU_CHOICES,
+        help_text=_("Menu in which this item will be displayed."),
     )
     css_class = forms.CharField(
-        label=_("CSS Class"),
-        help_text=_(
-            "Optional CSS class used to customize this link appearance in templates."
-        ),
+        label=_("CSS class"),
+        help_text=_('If you want to set custom value for link\'s "class".'),
         required=False,
     )
-    target = forms.CharField(
-        label=_("Target"),
+    target_blank = YesNoSwitch(
+        label=_("Open this link in new window"),
         help_text=_(
-            "Optional target attribute that this link will use (ex. '_blank')."
+            'Enabling this option will result in the target="_blank" attribute being '
+            "added to this link's HTML element."
         ),
         required=False,
     )
     rel = forms.CharField(
-        label=_("Rel"),
-        help_text=_("Optional rel attribute that this link will use (ex. 'nofollow')."),
+        label=_("Rel attribute"),
+        help_text=_(
+            'Optional "rel" attribute that this item will use (ex. "nofollow").'
+        ),
         required=False,
     )
 
     class Meta:
-        model = MenuLink
-        fields = ["link", "title", "position", "css_class", "target", "rel"]
+        model = MenuItem
+        fields = ["title", "url", "menu", "css_class", "target_blank", "rel"]
 
     def save(self):
-        link = super().save()
+        item = super().save()
         clear_menus_cache()
-        return link
+        return item

+ 2 - 2
misago/menus/admin/ordering.py

@@ -1,8 +1,8 @@
-from ..models import MenuLink
+from ..models import MenuItem
 
 
 def get_next_free_order():
-    last = MenuLink.objects.last()
+    last = MenuItem.objects.last()
     if last:
         return last.order + 1
     return 0

+ 11 - 14
misago/menus/admin/tests/conftest.py

@@ -1,32 +1,29 @@
 import pytest
 from django.urls import reverse
 
-from ...models import MenuLink
+from ...models import MenuItem
 
 
 @pytest.fixture
 def list_url(admin_client):
-    return reverse("misago:admin:settings:links:index")
+    return reverse("misago:admin:settings:menu-items:index")
 
 
 @pytest.fixture
-def menu_link(superuser):
-    return MenuLink.objects.create(
-        position=MenuLink.POSITION_TOP,
+def menu_item(superuser):
+    return MenuItem.objects.create(
+        menu=MenuItem.MENU_NAVBAR,
         title="Test TMLA",
-        link="https://top_menu_link_admin.com",
+        url="https://top_menu_item_admin.com",
         order=0,
     )
 
 
 @pytest.fixture
-def other_menu_link(superuser):
-    return MenuLink.objects.create(
-        position=MenuLink.POSITION_BOTH,
-        title="Other Menu Link",
-        link="https://other_menu_link.com",
-        css_class="other-menu-link",
-        rel="noopener noreferrer",
-        target="_blank",
+def other_menu_item(superuser):
+    return MenuItem.objects.create(
+        menu=MenuItem.MENU_BOTH,
+        title="Other Menu Item",
+        url="https://other_menu_item.com",
         order=1,
     )

+ 40 - 40
misago/menus/admin/tests/test_admin_views.py

@@ -2,12 +2,12 @@ import pytest
 from django.urls import reverse
 
 from ....test import assert_contains
-from ...models import MenuLink
+from ...models import MenuItem
 
 
-def test_nav_contains_menus_link(admin_client, list_url):
+def test_nav_contains_menus_item(admin_client, list_url):
     response = admin_client.get(list_url)
-    assert_contains(response, reverse("misago:admin:settings:links:index"))
+    assert_contains(response, reverse("misago:admin:settings:menu-items:index"))
 
 
 def test_empty_list_renders(admin_client, list_url):
@@ -15,80 +15,80 @@ def test_empty_list_renders(admin_client, list_url):
     assert response.status_code == 200
 
 
-def test_list_renders_menu_link(admin_client, list_url, menu_link):
+def test_list_renders_menu_item(admin_client, list_url, menu_item):
     response = admin_client.get(list_url)
-    assert_contains(response, menu_link.title)
+    assert_contains(response, menu_item.title)
 
 
-def test_menu_links_can_be_mass_deleted(admin_client, list_url, superuser):
-    links = []
+def test_menu_items_can_be_mass_deleted(admin_client, list_url, superuser):
+    items = []
     for _ in range(10):
-        link = MenuLink.objects.create(
-            position=MenuLink.POSITION_FOOTER,
-            title="Test Link {}".format(_),
-            link="https://links{}.com".format(_),
+        item = MenuItem.objects.create(
+            menu=MenuItem.MENU_FOOTER,
+            title="Test Item {}".format(_),
+            url="https://items{}.com".format(_),
         )
-        links.append(link.pk)
+        items.append(item.pk)
 
-    assert MenuLink.objects.count() == 10
+    assert MenuItem.objects.count() == 10
 
     response = admin_client.post(
-        list_url, data={"action": "delete", "selected_items": links}
+        list_url, data={"action": "delete", "selected_items": items}
     )
     assert response.status_code == 302
-    assert MenuLink.objects.count() == 0
+    assert MenuItem.objects.count() == 0
 
 
 def test_creation_form_renders(admin_client):
-    response = admin_client.get(reverse("misago:admin:settings:links:new"))
+    response = admin_client.get(reverse("misago:admin:settings:menu-items:new"))
     assert response.status_code == 200
 
 
-def test_form_creates_new_menu_link(admin_client):
+def test_form_creates_new_menu_item(admin_client):
     response = admin_client.post(
-        reverse("misago:admin:settings:links:new"),
+        reverse("misago:admin:settings:menu-items:new"),
         {
-            "position": MenuLink.POSITION_FOOTER,
-            "title": "Test Link",
-            "link": "https://admin.com/links/",
+            "menu": MenuItem.MENU_FOOTER,
+            "title": "Test Item",
+            "url": "https://admin.com/items/",
         },
     )
 
-    link = MenuLink.objects.get()
-    assert link.position == MenuLink.POSITION_FOOTER
-    assert link.title == "Test Link"
-    assert link.link == "https://admin.com/links/"
+    item = MenuItem.objects.get()
+    assert item.menu == MenuItem.MENU_FOOTER
+    assert item.title == "Test Item"
+    assert item.url == "https://admin.com/items/"
 
 
-def test_edit_form_renders(admin_client, menu_link):
+def test_edit_form_renders(admin_client, menu_item):
     response = admin_client.get(
-        reverse("misago:admin:settings:links:edit", kwargs={"pk": menu_link.pk})
+        reverse("misago:admin:settings:menu-items:edit", kwargs={"pk": menu_item.pk})
     )
-    assert_contains(response, menu_link.title)
+    assert_contains(response, menu_item.title)
 
 
-def test_edit_form_updates_menu_links(admin_client, menu_link):
+def test_edit_form_updates_menu_items(admin_client, menu_item):
     response = admin_client.post(
-        reverse("misago:admin:settings:links:edit", kwargs={"pk": menu_link.pk}),
+        reverse("misago:admin:settings:menu-items:edit", kwargs={"pk": menu_item.pk}),
         data={
-            "position": menu_link.POSITION_BOTH,
+            "menu": menu_item.MENU_BOTH,
             "title": "Test Edited",
-            "link": "https://example.com/edited/",
+            "url": "https://example.com/edited/",
         },
     )
     assert response.status_code == 302
 
-    menu_link.refresh_from_db()
-    assert menu_link.position == MenuLink.POSITION_BOTH
-    assert menu_link.title == "Test Edited"
-    assert menu_link.link == "https://example.com/edited/"
+    menu_item.refresh_from_db()
+    assert menu_item.menu == MenuItem.MENU_BOTH
+    assert menu_item.title == "Test Edited"
+    assert menu_item.url == "https://example.com/edited/"
 
 
-def test_menu_link_can_be_deleted(admin_client, menu_link):
+def test_menu_item_can_be_deleted(admin_client, menu_item):
     response = admin_client.post(
-        reverse("misago:admin:settings:links:delete", kwargs={"pk": menu_link.pk})
+        reverse("misago:admin:settings:menu-items:delete", kwargs={"pk": menu_item.pk})
     )
     assert response.status_code == 302
 
-    with pytest.raises(MenuLink.DoesNotExist):
-        menu_link.refresh_from_db()
+    with pytest.raises(MenuItem.DoesNotExist):
+        menu_item.refresh_from_db()

+ 59 - 57
misago/menus/admin/tests/test_ordering_menu_links.py

@@ -1,102 +1,104 @@
 from django.urls import reverse
 
 from ....cache.test import assert_invalidates_cache
-from ... import MENU_LINKS_CACHE
+from ... import MENU_ITEMS_CACHE
 
 
-def test_top_menu_link_can_be_moved_down(admin_client, menu_link, other_menu_link):
-    menu_link.order = 0
-    menu_link.save()
+def test_top_menu_item_can_be_moved_down(admin_client, menu_item, other_menu_item):
+    menu_item.order = 0
+    menu_item.save()
 
-    other_menu_link.order = 1
-    other_menu_link.save()
+    other_menu_item.order = 1
+    other_menu_item.save()
 
     admin_client.post(
-        reverse("misago:admin:settings:links:down", kwargs={"pk": menu_link.pk})
+        reverse("misago:admin:settings:menu-items:down", kwargs={"pk": menu_item.pk})
     )
 
-    menu_link.refresh_from_db()
-    assert menu_link.order == 1
-    other_menu_link.refresh_from_db()
-    assert other_menu_link.order == 0
+    menu_item.refresh_from_db()
+    assert menu_item.order == 1
+    other_menu_item.refresh_from_db()
+    assert other_menu_item.order == 0
 
 
-def test_top_menu_link_cant_be_moved_up(admin_client, menu_link, other_menu_link):
-    menu_link.order = 0
-    menu_link.save()
+def test_top_menu_item_cant_be_moved_up(admin_client, menu_item, other_menu_item):
+    menu_item.order = 0
+    menu_item.save()
 
-    other_menu_link.order = 1
-    other_menu_link.save()
+    other_menu_item.order = 1
+    other_menu_item.save()
 
     admin_client.post(
-        reverse("misago:admin:settings:links:up", kwargs={"pk": menu_link.pk})
+        reverse("misago:admin:settings:menu-items:up", kwargs={"pk": menu_item.pk})
     )
 
-    menu_link.refresh_from_db()
-    assert menu_link.order == 0
-    other_menu_link.refresh_from_db()
-    assert other_menu_link.order == 1
+    menu_item.refresh_from_db()
+    assert menu_item.order == 0
+    other_menu_item.refresh_from_db()
+    assert other_menu_item.order == 1
 
 
-def test_bottom_menu_link_cant_be_moved_down(admin_client, menu_link, other_menu_link):
-    menu_link.order = 1
-    menu_link.save()
+def test_bottom_menu_item_cant_be_moved_down(admin_client, menu_item, other_menu_item):
+    menu_item.order = 1
+    menu_item.save()
 
-    other_menu_link.order = 0
-    other_menu_link.save()
+    other_menu_item.order = 0
+    other_menu_item.save()
 
     admin_client.post(
-        reverse("misago:admin:settings:links:down", kwargs={"pk": menu_link.pk})
+        reverse("misago:admin:settings:menu-items:down", kwargs={"pk": menu_item.pk})
     )
 
-    menu_link.refresh_from_db()
-    assert menu_link.order == 1
-    other_menu_link.refresh_from_db()
-    assert other_menu_link.order == 0
+    menu_item.refresh_from_db()
+    assert menu_item.order == 1
+    other_menu_item.refresh_from_db()
+    assert other_menu_item.order == 0
 
 
-def test_bottom_menu_link_can_be_moved_up(admin_client, menu_link, other_menu_link):
-    menu_link.order = 1
-    menu_link.save()
+def test_bottom_menu_item_can_be_moved_up(admin_client, menu_item, other_menu_item):
+    menu_item.order = 1
+    menu_item.save()
 
-    other_menu_link.order = 0
-    other_menu_link.save()
+    other_menu_item.order = 0
+    other_menu_item.save()
 
     admin_client.post(
-        reverse("misago:admin:settings:links:up", kwargs={"pk": menu_link.pk})
+        reverse("misago:admin:settings:menu-items:up", kwargs={"pk": menu_item.pk})
     )
 
-    menu_link.refresh_from_db()
-    assert menu_link.order == 0
-    other_menu_link.refresh_from_db()
-    assert other_menu_link.order == 1
+    menu_item.refresh_from_db()
+    assert menu_item.order == 0
+    other_menu_item.refresh_from_db()
+    assert other_menu_item.order == 1
 
 
-def test_moving_menu_link_down_invalidates_menu_links_cache(
-    admin_client, menu_link, other_menu_link
+def test_moving_menu_item_down_invalidates_menu_items_cache(
+    admin_client, menu_item, other_menu_item
 ):
-    menu_link.order = 0
-    menu_link.save()
+    menu_item.order = 0
+    menu_item.save()
 
-    other_menu_link.order = 1
-    other_menu_link.save()
+    other_menu_item.order = 1
+    other_menu_item.save()
 
-    with assert_invalidates_cache(MENU_LINKS_CACHE):
+    with assert_invalidates_cache(MENU_ITEMS_CACHE):
         admin_client.post(
-            reverse("misago:admin:settings:links:down", kwargs={"pk": menu_link.pk})
+            reverse(
+                "misago:admin:settings:menu-items:down", kwargs={"pk": menu_item.pk}
+            )
         )
 
 
-def test_moving_menu_link_up_invalidates_menu_links_cache(
-    admin_client, menu_link, other_menu_link
+def test_moving_menu_item_up_invalidates_menu_items_cache(
+    admin_client, menu_item, other_menu_item
 ):
-    menu_link.order = 1
-    menu_link.save()
+    menu_item.order = 1
+    menu_item.save()
 
-    other_menu_link.order = 0
-    other_menu_link.save()
+    other_menu_item.order = 0
+    other_menu_item.save()
 
-    with assert_invalidates_cache(MENU_LINKS_CACHE):
+    with assert_invalidates_cache(MENU_ITEMS_CACHE):
         admin_client.post(
-            reverse("misago:admin:settings:links:up", kwargs={"pk": menu_link.pk})
+            reverse("misago:admin:settings:menu-items:up", kwargs={"pk": menu_item.pk})
         )

+ 30 - 33
misago/menus/admin/views.py

@@ -2,47 +2,44 @@ from django.contrib import messages
 from django.utils.translation import gettext_lazy as _
 
 from ...admin.views import generic
-from ..models import MenuLink
+from ..models import MenuItem
 from ..cache import clear_menus_cache
-
-from .forms import MenuLinkForm
+from .forms import MenuItemForm
 from .ordering import get_next_free_order
 
 
-class MenuLinkAdmin(generic.AdminBaseMixin):
-    root_link = "misago:admin:settings:links:index"
-    model = MenuLink
-    form_class = MenuLinkForm
-    templates_dir = "misago/admin/menulinks"
-    message_404 = _("Requested MenuLink does not exist.")
+class MenuItemAdmin(generic.AdminBaseMixin):
+    root_link = "misago:admin:settings:menu-items:index"
+    model = MenuItem
+    form_class = MenuItemForm
+    templates_dir = "misago/admin/menuitems"
+    message_404 = _("Requested menu item does not exist.")
 
     def handle_form(self, form, request, target):
         form.save()
 
         if self.message_submit:
-            messages.success(request, self.message_submit % {"title": target.title})
+            messages.success(request, self.message_submit % {"item": target})
 
 
-class MenuLinksList(MenuLinkAdmin, generic.ListView):
+class MenuItemsList(MenuItemAdmin, generic.ListView):
     ordering = (("order", None),)
-    selection_label = _("With MenuLinks: 0")
-    empty_selection_label = _("Select MenuLinks")
     mass_actions = [
         {
             "action": "delete",
-            "name": _("Delete MenuLinks"),
-            "confirmation": _("Are you sure you want to delete those MenuLinks?"),
+            "name": _("Delete items"),
+            "confirmation": _("Are you sure you want to delete those menu items?"),
         }
     ]
 
     def action_delete(self, request, items):
         items.delete()
         clear_menus_cache()
-        messages.success(request, _("Selected MenuLinks have been deleted."))
+        messages.success(request, _("Selected menu items have been deleted."))
 
 
-class NewMenuLink(MenuLinkAdmin, generic.ModelFormView):
-    message_submit = _('New MenuLink "%(title)s" has been saved.')
+class NewMenuItem(MenuItemAdmin, generic.ModelFormView):
+    message_submit = _("New menu item %(item)s has been saved.")
 
     def handle_form(self, form, request, target):
         super().handle_form(form, request, target)
@@ -51,8 +48,8 @@ class NewMenuLink(MenuLinkAdmin, generic.ModelFormView):
         clear_menus_cache()
 
 
-class EditMenuLink(MenuLinkAdmin, generic.ModelFormView):
-    message_submit = _('MenuLink "%(title)s" has been edited.')
+class EditMenuItem(MenuItemAdmin, generic.ModelFormView):
+    message_submit = _("Menu item %(item)s has been edited.")
 
     def handle_form(self, form, request, target):
         super().handle_form(form, request, target)
@@ -60,20 +57,20 @@ class EditMenuLink(MenuLinkAdmin, generic.ModelFormView):
         clear_menus_cache()
 
 
-class DeleteMenuLink(MenuLinkAdmin, generic.ButtonView):
+class DeleteMenuItem(MenuItemAdmin, generic.ButtonView):
     def button_action(self, request, target):
         target.delete()
         clear_menus_cache()
-        message = _('MenuLink "%(title)s" has been deleted.')
-        messages.success(request, message % {"title": target.title})
+        message = _("Menu item %(item)s has been deleted.")
+        messages.success(request, message % {"item": target})
 
 
-class MoveDownMenuLink(MenuLinkAdmin, generic.ButtonView):
+class MoveDownMenuItem(MenuItemAdmin, generic.ButtonView):
     def button_action(self, request, target):
         try:
-            other_target = MenuLink.objects.filter(order__gt=target.order)
+            other_target = MenuItem.objects.filter(order__gt=target.order)
             other_target = other_target.earliest("order")
-        except MenuLink.DoesNotExist:
+        except MenuItem.DoesNotExist:
             other_target = None
 
         if other_target:
@@ -82,17 +79,17 @@ class MoveDownMenuLink(MenuLinkAdmin, generic.ButtonView):
             target.save(update_fields=["order"])
             clear_menus_cache()
 
-            message = _("Menu link to %(link)s has been moved after %(other)s.")
-            targets_names = {"link": target, "other": other_target}
+            message = _("Menu item %(item)s has been moved after %(other)s.")
+            targets_names = {"item": target, "other": other_target}
             messages.success(request, message % targets_names)
 
 
-class MoveUpMenuLink(MenuLinkAdmin, generic.ButtonView):
+class MoveUpMenuItem(MenuItemAdmin, generic.ButtonView):
     def button_action(self, request, target):
         try:
-            other_target = MenuLink.objects.filter(order__lt=target.order)
+            other_target = MenuItem.objects.filter(order__lt=target.order)
             other_target = other_target.latest("order")
-        except MenuLink.DoesNotExist:
+        except MenuItem.DoesNotExist:
             other_target = None
 
         if other_target:
@@ -101,6 +98,6 @@ class MoveUpMenuLink(MenuLinkAdmin, generic.ButtonView):
             target.save(update_fields=["order"])
             clear_menus_cache()
 
-            message = _("Menu link to %(link)s has been moved before %(other)s.")
-            targets_names = {"link": target, "other": other_target}
+            message = _("Menu item %(item)s has been moved before %(other)s.")
+            targets_names = {"item": target, "other": other_target}
             messages.success(request, message % targets_names)

+ 5 - 5
misago/menus/cache.py

@@ -1,7 +1,7 @@
 from django.core.cache import cache
 
-from . import MENU_LINKS_CACHE
 from ..cache.versions import invalidate_cache
+from . import MENU_ITEMS_CACHE
 
 
 def get_menus_cache(cache_versions):
@@ -9,14 +9,14 @@ def get_menus_cache(cache_versions):
     return cache.get(key)
 
 
-def set_menus_cache(cache_versions, menu_links):
+def set_menus_cache(cache_versions, menus):
     key = get_cache_key(cache_versions)
-    cache.set(key, menu_links)
+    cache.set(key, menus)
 
 
 def get_cache_key(cache_versions):
-    return "%s_%s" % (MENU_LINKS_CACHE, cache_versions[MENU_LINKS_CACHE])
+    return "%s_%s" % (MENU_ITEMS_CACHE, cache_versions[MENU_ITEMS_CACHE])
 
 
 def clear_menus_cache():
-    invalidate_cache(MENU_LINKS_CACHE)
+    invalidate_cache(MENU_ITEMS_CACHE)

+ 6 - 7
misago/menus/context_processors.py

@@ -1,9 +1,8 @@
-from .menu_links import get_top_menu_links, get_footer_menu_links
+from .menuitems import get_footer_menu_items, get_navbar_menu_items
 
 
-def navbar(request):
-    return {"top_menu_links": get_top_menu_links(request.cache_versions)}
-
-
-def footer(request):
-    return {"footer_menu_links": get_footer_menu_links(request.cache_versions)}
+def menus(request):
+    return {
+        "navbar_menu": get_navbar_menu_items(request.cache_versions),
+        "footer_menu": get_footer_menu_items(request.cache_versions),
+    }

+ 0 - 40
misago/menus/menu_links.py

@@ -1,40 +0,0 @@
-from django.db.models import Q
-
-from .cache import set_menus_cache, get_menus_cache
-from .models import MenuLink
-
-
-def get_top_menu_links(cache_versions):
-    return get_links(cache_versions).get(MenuLink.POSITION_TOP)
-
-
-def get_footer_menu_links(cache_versions):
-    return get_links(cache_versions).get(MenuLink.POSITION_FOOTER)
-
-
-def get_links(cache_versions):
-    links = get_menus_cache(cache_versions)
-    if links is None:
-        links = get_links_from_db()
-        set_menus_cache(cache_versions, links)
-    return links
-
-
-def get_links_from_db():
-    links = {
-        MenuLink.POSITION_TOP: _get_footer_menu_links_from_db(),
-        MenuLink.POSITION_FOOTER: _get_top_menu_links_from_db(),
-    }
-    return links
-
-
-def _get_footer_menu_links_from_db():
-    return MenuLink.objects.filter(
-        Q(position=MenuLink.POSITION_TOP) | Q(position=MenuLink.POSITION_BOTH)
-    ).values()
-
-
-def _get_top_menu_links_from_db():
-    return MenuLink.objects.filter(
-        Q(position=MenuLink.POSITION_FOOTER) | Q(position=MenuLink.POSITION_BOTH)
-    ).values()

+ 33 - 0
misago/menus/menuitems.py

@@ -0,0 +1,33 @@
+from .cache import set_menus_cache, get_menus_cache
+from .models import MenuItem
+
+
+def get_navbar_menu_items(cache_versions):
+    return get_items(cache_versions).get(MenuItem.MENU_NAVBAR)
+
+
+def get_footer_menu_items(cache_versions):
+    return get_items(cache_versions).get(MenuItem.MENU_FOOTER)
+
+
+def get_items(cache_versions):
+    items = get_menus_cache(cache_versions)
+    if items is None:
+        items = get_items_from_db()
+        set_menus_cache(cache_versions, items)
+    return items
+
+
+def get_items_from_db():
+    return {
+        MenuItem.MENU_NAVBAR: get_navbar_menu_items_from_db(),
+        MenuItem.MENU_FOOTER: get_footer_menu_items_from_db(),
+    }
+
+
+def get_navbar_menu_items_from_db():
+    return MenuItem.objects.exclude(menu=MenuItem.MENU_FOOTER).values()
+
+
+def get_footer_menu_items_from_db():
+    return MenuItem.objects.exclude(menu=MenuItem.MENU_NAVBAR).values()

+ 13 - 20
misago/menus/migrations/0001_initial.py

@@ -1,19 +1,17 @@
-# Generated by Django 2.2.3 on 2019-09-11 07:13
+# Generated by Django 2.2.3 on 2019-09-15 19:15
 
 from django.db import migrations, models
-from ...cache.operations import StartCacheVersioning
-from .. import MENU_LINKS_CACHE
 
 
 class Migration(migrations.Migration):
 
     initial = True
 
-    dependencies = [("misago_cache", "0001_initial")]
+    dependencies = []
 
     operations = [
         migrations.CreateModel(
-            name="MenuLink",
+            name="MenuItem",
             fields=[
                 (
                     "id",
@@ -24,29 +22,24 @@ class Migration(migrations.Migration):
                         verbose_name="ID",
                     ),
                 ),
-                ("link", models.URLField()),
-                ("title", models.CharField(max_length=150)),
+                ("title", models.CharField(max_length=255)),
+                ("url", models.URLField()),
                 (
-                    "position",
+                    "menu",
                     models.CharField(
                         choices=[
-                            ("top", "Header navbar"),
+                            ("both", "Navbar and footer"),
+                            ("navbar", "Navbar"),
                             ("footer", "Footer"),
-                            ("both", "Header and footer"),
                         ],
-                        max_length=20,
+                        max_length=6,
                     ),
                 ),
                 ("order", models.IntegerField(default=0)),
                 ("css_class", models.CharField(blank=True, max_length=255, null=True)),
-                ("target", models.CharField(blank=True, max_length=100, null=True)),
-                ("rel", models.CharField(blank=True, max_length=100, null=True)),
+                ("target_blank", models.BooleanField(default=False)),
+                ("rel", models.CharField(blank=True, max_length=255, null=True)),
             ],
-            options={
-                "ordering": ("order",),
-                "get_latest_by": "order",
-                "unique_together": {("link", "position")},
-            },
-        ),
-        StartCacheVersioning(MENU_LINKS_CACHE),
+            options={"ordering": ("order",), "get_latest_by": "order"},
+        )
     ]

+ 12 - 0
misago/menus/migrations/0002_cache_version.py

@@ -0,0 +1,12 @@
+# Generated by Django 1.11.16 on 2018-12-02 15:54
+from django.db import migrations
+
+from .. import MENU_ITEMS_CACHE
+from ...cache.operations import StartCacheVersioning
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [("misago_menus", "0001_initial"), ("misago_cache", "0001_initial")]
+
+    operations = [StartCacheVersioning(MENU_ITEMS_CACHE)]

+ 14 - 21
misago/menus/models.py

@@ -2,34 +2,27 @@ from django.db import models
 from django.utils.translation import gettext_lazy as _
 
 
-class MenuLinkManager(models.Manager):
-    pass
-
-
-class MenuLink(models.Model):
-    POSITION_TOP = "top"
-    POSITION_FOOTER = "footer"
-    POSITION_BOTH = "both"
-    LINK_POSITION_CHOICES = [
-        (POSITION_TOP, _("Header navbar")),
-        (POSITION_FOOTER, _("Footer")),
-        (POSITION_BOTH, _("Header and footer")),
+class MenuItem(models.Model):
+    MENU_BOTH = "both"
+    MENU_NAVBAR = "navbar"
+    MENU_FOOTER = "footer"
+    MENU_CHOICES = [
+        (MENU_BOTH, _("Navbar and footer")),
+        (MENU_NAVBAR, _("Navbar")),
+        (MENU_FOOTER, _("Footer")),
     ]
 
-    link = models.URLField()
-    title = models.CharField(max_length=150)
-    position = models.CharField(max_length=20, choices=LINK_POSITION_CHOICES)
+    menu = models.CharField(max_length=6, choices=MENU_CHOICES)
+    title = models.CharField(max_length=255)
+    url = models.URLField()
     order = models.IntegerField(default=0)
     css_class = models.CharField(max_length=255, null=True, blank=True)
-    target = models.CharField(max_length=100, null=True, blank=True)
-    rel = models.CharField(max_length=100, null=True, blank=True)
-
-    objects = MenuLinkManager()
+    target_blank = models.BooleanField(default=False)
+    rel = models.CharField(max_length=255, null=True, blank=True)
 
     class Meta:
-        unique_together = ("link", "position")
         ordering = ("order",)
         get_latest_by = "order"
 
     def __str__(self):
-        return self.link
+        return self.title

+ 30 - 30
misago/menus/tests/conftest.py

@@ -1,57 +1,57 @@
 import pytest
 
-from ..models import MenuLink
-from ..menu_links import get_footer_menu_links, get_top_menu_links
+from ..menuitems import get_footer_menu_items, get_navbar_menu_items
+from ..models import MenuItem
 
 
 @pytest.fixture
-def menu_link_top(db):
-    return MenuLink.objects.create(
-        link="https://top_menu_link.com",
-        title="Top Menu Link",
-        position=MenuLink.POSITION_TOP,
+def navar_menu_item(db):
+    return MenuItem.objects.create(
+        title="Top Menu Item",
+        url="https://navbar_menu_item.com",
+        menu=MenuItem.MENU_NAVBAR,
     )
 
 
 @pytest.fixture
-def menu_link_footer(db):
-    return MenuLink.objects.create(
-        link="https://footer_menu_link.com",
-        title="Footer Menu Link",
-        position=MenuLink.POSITION_FOOTER,
+def footer_menu_item(db):
+    return MenuItem.objects.create(
+        title="Footer Menu Item",
+        url="https://footer_menu_item.com",
+        menu=MenuItem.MENU_FOOTER,
     )
 
 
 @pytest.fixture
-def menu_link_both(db):
-    return MenuLink.objects.create(
-        link="https://both_positions_menu_link.com",
-        title="Both Positions Menu Link",
-        position=MenuLink.POSITION_BOTH,
+def both_menus_item(db):
+    return MenuItem.objects.create(
+        title="Both Positions Menu Item",
+        url="https://both_menus_menu_item.com",
+        menu=MenuItem.MENU_BOTH,
     )
 
 
 @pytest.fixture
-def menu_link_with_attributes(db):
-    return MenuLink.objects.create(
-        link="https://menu_link_with_attributes.com",
-        title="Menu link with attributes",
-        position=MenuLink.POSITION_BOTH,
+def menu_item_with_attributes(db):
+    return MenuItem.objects.create(
+        title="Menu item with attributes",
+        url="https://menu_item_with_attributes.com",
+        menu=MenuItem.MENU_BOTH,
         rel="noopener nofollow",
-        target="_blank",
-        css_class="test-link-css-class",
+        target_blank=True,
+        css_class="test-item-css-class",
     )
 
 
 @pytest.fixture
-def links_footer(
-    db, cache_versions, menu_link_footer, menu_link_both, menu_link_with_attributes
+def navbar_menu_items(
+    db, cache_versions, navar_menu_item, both_menus_item, menu_item_with_attributes
 ):
-    return get_footer_menu_links(cache_versions)
+    return get_navbar_menu_items(cache_versions)
 
 
 @pytest.fixture
-def links_top(
-    db, cache_versions, menu_link_top, menu_link_both, menu_link_with_attributes
+def footer_menu_items(
+    db, cache_versions, footer_menu_item, both_menus_item, menu_item_with_attributes
 ):
-    return get_top_menu_links(cache_versions)
+    return get_footer_menu_items(cache_versions)

+ 11 - 7
misago/menus/tests/test_context_processor.py

@@ -1,15 +1,19 @@
 from unittest.mock import Mock
 
-from ..context_processors import footer, navbar
+from ..context_processors import menus
 
 
-def test_footer_menu_links_context_processor(links_footer, cache_versions):
-    result = footer(Mock(cache_versions=cache_versions))
+def test_context_processor_adds_navbar_menu_to_context(
+    navbar_menu_items, cache_versions
+):
+    result = menus(Mock(cache_versions=cache_versions))
     assert isinstance(result, dict)
-    assert len(links_footer) == len(result["footer_menu_links"])
+    assert len(navbar_menu_items) == len(result["navbar_menu"])
 
 
-def test_top_menu_links_context_processor(links_top, cache_versions):
-    result = navbar(Mock(cache_versions=cache_versions))
+def test_context_processor_adds_footer_menu_to_context(
+    footer_menu_items, cache_versions
+):
+    result = menus(Mock(cache_versions=cache_versions))
     assert isinstance(result, dict)
-    assert len(links_top) == len(result["top_menu_links"])
+    assert len(footer_menu_items) == len(result["footer_menu"])

+ 0 - 44
misago/menus/tests/test_menulink_frontend.py

@@ -1,44 +0,0 @@
-from django.urls import reverse
-from bs4 import BeautifulSoup
-
-
-def test_top_menu_link_in_frontend(client, menu_link_top):
-    response = client.get(reverse("misago:index"))
-    assert response.status_code == 200
-    parser = BeautifulSoup(response.content, "html.parser")
-    link = parser.find("a", href=menu_link_top.link)
-    assert link is not None
-    assert menu_link_top.title == link.getText().strip()
-
-
-def test_footer_menu_link_in_frontend(client, menu_link_footer):
-    response = client.get(reverse("misago:index"))
-    assert response.status_code == 200
-    parser = BeautifulSoup(response.content, "html.parser")
-    link = parser.find("a", href=menu_link_footer.link)
-    assert link is not None
-    assert menu_link_footer.title == link.getText().strip()
-
-
-def test_both_menus_link_in_frontend(client, menu_link_both):
-    response = client.get(reverse("misago:index"))
-    assert response.status_code == 200
-    parser = BeautifulSoup(response.content, "html.parser")
-    links = parser.find_all("a", href=menu_link_both.link)
-    assert links != []
-    assert len(links) == 2
-    for link in links:
-        assert link is not None
-        assert menu_link_both.title == link.getText().strip()
-
-
-def test_menu_link_attributes_in_frontend(client, menu_link_with_attributes):
-    response = client.get(reverse("misago:index"))
-    assert response.status_code == 200
-    parser = BeautifulSoup(response.content, "html.parser")
-    link = parser.find("a", href=menu_link_with_attributes.link)
-    assert link is not None
-    assert menu_link_with_attributes.title == link.getText().strip()
-    assert menu_link_with_attributes.rel.split() == link["rel"]
-    assert menu_link_with_attributes.target == link["target"]
-    assert menu_link_with_attributes.css_class.split() == link["class"]

+ 18 - 0
misago/menus/tests/test_menus_rendering.py

@@ -0,0 +1,18 @@
+from django.urls import reverse
+
+from ...test import assert_contains
+
+
+def test_custom_menu_items_are_rendered_in_navbar(client, navar_menu_item):
+    response = client.get(reverse("misago:index"))
+    assert_contains(response, navar_menu_item.url)
+
+
+def test_custom_menu_items_are_rendered_in_footer(client, footer_menu_item):
+    response = client.get(reverse("misago:index"))
+    assert_contains(response, footer_menu_item.url)
+
+
+def test_custom_menu_items_are_rendered_in_both_menus(client, both_menus_item):
+    response = client.get(reverse("misago:index"))
+    assert_contains(response, both_menus_item.url)

+ 10 - 13
misago/templates/misago/admin/menulinks/form.html → misago/templates/misago/admin/menuitems/form.html

@@ -6,7 +6,7 @@
 {% if target.pk %}
 {% trans target.title %}
 {% else %}
-{% trans "New menu link" %}
+{% trans "New menu item" %}
 {% endif %} | {{ active_link.name }} | {{ block.super }}
 {% endblock title %}
 
@@ -15,7 +15,7 @@
 {{ block.super }}
 {% if target.pk %}
   <small>
-    {{ target.title }}
+    {{ target }}
   </small>
 {% endif %}
 {% endblock page-header %}
@@ -28,9 +28,9 @@
 
 {% block form-header %}
 {% if target.pk %}
-  {% trans "Edit link" %}
+  {% trans "Edit menu item" %}
 {% else %}
-  {% trans "New link" %}
+  {% trans "New menu item" %}
 {% endif %}
 {% endblock %}
 
@@ -40,21 +40,18 @@
   <fieldset>
     <legend>{% trans "Basic settings" %}</legend>
 
-    {% form_row form.link %}
     {% form_row form.title %}
-    {% form_row form.position %}
-
+    {% form_row form.url %}
+    
   </fieldset>
 </div>
 <div class="form-fieldset">
   <fieldset>
-    <legend>{% trans "Extra configuration" %}</legend>
-
-    <div class="alert alert-info" role="alert">
-      {% trans "Optional link configurations." %}
-    </div>
+    <legend>{% trans "Behavior and appearance" %}</legend>
+    
+    {% form_row form.menu %}
     {% form_row form.css_class %}
-    {% form_row form.target %}
+    {% form_row form.target_blank %}
     {% form_row form.rel %}
 
   </fieldset>

+ 46 - 47
misago/templates/misago/admin/menulinks/list.html → misago/templates/misago/admin/menuitems/list.html

@@ -4,21 +4,20 @@
 
 {% block page-actions %}
 <div class="col-auto page-action">
-  <a href="{% url 'misago:admin:settings:links:new' %}" class="btn btn-primary btn-sm">
+  <a href="{% url 'misago:admin:settings:menu-items:new' %}" class="btn btn-primary btn-sm">
     <span class="fa fa-plus-circle"></span>
-    {% trans "New menu link" %}
+    {% trans "New menu item" %}
   </a>
 </div>
 {% endblock %}
 
 
 {% block table-header %}
-<th>{% trans "Menu Link" %}</th>
-<th >{% trans "Link" %}</th>
-<th style="width: 180px;">{% trans "Position" %}</th>
-<th style="width: 250px;">{% trans "CSS Class" %}</th>
-<th style="width: 250px;">{% trans "Target" %}</th>
-<th style="width: 250px;">{% trans "Rel" %}</th>
+<th>{% trans "Item" %}</th>
+<th style="width: 1px;">&nbsp;</th>
+<th style="width: 170px;">{% trans "CSS Class" %}</th>
+<th style="width: 170px;">{% trans "Target" %}</th>
+<th style="width: 200px;">{% trans "Rel" %}</th>
 <th style="width: 1px;">&nbsp;</th>
 <th style="width: 1px;">&nbsp;</th>
 <th style="width: 1px;">&nbsp;</th>
@@ -26,23 +25,41 @@
 
 
 {% block table-row %}
-
-<td class="pr-0 small">
-  <a href="{% url 'misago:admin:settings:links:edit' pk=item.pk %}" class="item-name">
-    {{ item.title }}
-  </a>
-</td>
-<td class="small">
-  {{ item.link }}
+<td class="pr-0">
+  <div class="small">
+    <a href="{% url 'misago:admin:settings:menu-items:edit' pk=item.pk %}" class="item-name">
+      {{ item }}
+    </a>
+  </div>
+  <div class="small">
+    {{ item.url }}
+  </div>
 </td>
-<td class="small">
-  {{ item.get_position_display }}
+<td class="badges-list">
+  {% if item.menu == item.MENU_NAVBAR or item.menu == item.MENU_BOTH %}
+    <span class="badge badge-primary" data-tooltip="top" title="{% trans 'Is displayed on the forum navbar.' %}">
+      {% trans "Navbar" %}
+    </span>
+  {% endif %}
+  {% if item.menu == item.MENU_FOOTER or item.menu == item.MENU_BOTH %}
+    <span class="badge badge-info" data-tooltip="top" title="{% trans 'Is displayed on the forum footer.' %}">
+      {% trans "Footer" %}
+    </span>
+  {% endif %}
 </td>
 <td class="small">
-  {{ item.css_class }}
+  {% if item.css_class %}
+    <pre class="m-0">{{ item.css_class }}</pre>
+  {% else %}
+    <i class="text-muted">{% trans "Not set" %}</i>
+  {% endif %}
 </td>
 <td class="small">
-  {{ item.target }}
+  {% if item.target_blank %}
+    <pre class="m-0">target="_blank"</pre>
+  {% else %}
+    &nbsp;
+  {% endif %}
 </td>
 <td class="small">
   {{ item.rel }}
@@ -50,7 +67,7 @@
 {% include "misago/admin/generic/list_extra_actions.html" %}
 <td>
   {% if not forloop.last %}
-    <form action="{% url 'misago:admin:settings:links:down' pk=item.pk %}" method="post">
+    <form action="{% url 'misago:admin:settings:menu-items:down' pk=item.pk %}" method="post">
       {% csrf_token %}
       <button class="btn btn-light btn-sm" data-tooltip="top" title="{% trans 'Move down' %}">
         <span class="fa fa-chevron-down"></span>
@@ -64,7 +81,7 @@
 </td>
 <td>
   {% if not forloop.first %}
-    <form action="{% url 'misago:admin:settings:links:up' pk=item.pk %}" method="post">
+    <form action="{% url 'misago:admin:settings:menu-items:up' pk=item.pk %}" method="post">
       {% csrf_token %}
       <button class="btn btn-light btn-sm" data-tooltip="top" title="{% trans 'Move up' %}">
         <span class="fa fa-chevron-up"></span>
@@ -82,13 +99,13 @@
       <i class="fas fa-ellipsis-h"></i>
     </button>
     <div class="dropdown-menu dropdown-menu-right" aria-labelledby="item-optioms-{{ item.pk }}">
-      <a class="dropdown-item" href="{% url 'misago:admin:settings:links:edit' pk=item.pk %}">
-        {% trans "Edit menu link" %}
+      <a class="dropdown-item" href="{% url 'misago:admin:settings:menu-items:edit' pk=item.pk %}">
+        {% trans "Edit item" %}
       </a>
-      <form action="{% url 'misago:admin:settings:links:delete' pk=item.pk %}" method="post" data-delete-confirmation="true">
+      <form action="{% url 'misago:admin:settings:menu-items:delete' pk=item.pk %}" method="post" data-delete-confirmation="true">
         {% csrf_token %}
         <button class="dropdown-item">
-          {% trans "Remove menu link" %}
+          {% trans "Remove item" %}
         </button>
       </form>
     </div>
@@ -98,12 +115,8 @@
 
 
 {% block blankslate %}
-<td colspan="8">
-  {% if active_filters %}
-    {% trans "No menu links matching criteria exist." %}
-  {% else %}
-    {% trans "No menu links are set." %}
-  {% endif %}
+<td colspan="9">
+  {% trans "No menu items are set." %}
 </td>
 {% endblock blankslate %}
 
@@ -113,21 +126,7 @@
 <script type="text/javascript">
   window.misago.initConfirmation(
     "[data-delete-confirmation]",
-    "{% trans 'Are you sure you want to remove this menu link?' %}"
+    "{% trans 'Are you sure you want to remove this menu item?' %}"
   )
 </script>
 {% endblock %}
-
-
-{% block filters-modal-body %}
-<div class="row">
-  <div class="col">
-    {% form_row filter_form.position %}
-  </div>
-</div>
-<div class="row">
-  <div class="col">
-    {% form_row filter_form.content %}
-  </div>
-</div>
-{% endblock filters-modal-body %}

+ 5 - 5
misago/templates/misago/footer.html

@@ -10,18 +10,18 @@
         </p>
       </noscript>
 
-      {% if TERMS_OF_SERVICE_URL or PRIVACY_POLICY_URL or settings.forum_footnote or footer_menu_links %}
+      {% if TERMS_OF_SERVICE_URL or PRIVACY_POLICY_URL or settings.forum_footnote or footer_menu %}
         <ul class="list-inline footer-nav">
           {% if settings.forum_footnote %}
             <li class="site-footnote">
               {{ settings.forum_footnote }}
             </li>
           {% endif %}
-          {% for link in footer_menu_links %}
+          {% for link in footer_menu %}
             <li>
-                <a href="{{ link.link }}"{% if link.css_class %} class="{{ link.css_class}}"{% endif %}{% if link.rel %} rel="{{link.rel}}"{% endif %}{% if link.target %} target="{{link.target}}"{% endif %}>
-                  {{ link.title }}
-                </a>
+              <a href="{{ link.url }}"{% if link.css_class %} class="{{ link.css_class }}"{% endif %}{% if link.rel %} rel="{{ link.rel }}"{% endif %}{% if link.target_blank %} target="_blank"{% endif %}>
+                {{ link.title }}
+              </a>
             </li>
           {% endfor %}
           {% if TERMS_OF_SERVICE_URL %}

+ 4 - 4
misago/templates/misago/navbar.html

@@ -42,12 +42,12 @@
           {% trans "Users" %}
         </a>
       </li>
-      {% for link in top_menu_links %}
-      <li>
-          <a href="{{ link.link }}"{% if link.css_class %} class="{{ link.css_class}}"{% endif %}{% if link.rel %} rel="{{link.rel}}"{% endif %}{% if link.target %} target="{{link.target}}"{% endif %}>
+      {% for link in navbar_menu %}
+        <li>
+          <a href="{{ link.url }}"{% if link.css_class %} class="{{ link.css_class }}"{% endif %}{% if link.rel %} rel="{{ link.rel }}"{% endif %}{% if link.target_blank %} target="_blank"{% endif %}>
             {{ link.title }}
           </a>
-      </li>
+        </li>
       {% endfor %}
 
     </ul>