Commits

Kai Diefenbach committed 9e780fa

Added portlets and seo to pages.

  • Participants
  • Parent commits 970c7cc

Comments (0)

Files changed (28)

 *build/
 *dist/
 *.egg-info/
+*.git*

File lfs/catalog/models.py

     description = models.TextField(_(u"Description"), blank=True)
     file = models.FileField(upload_to="files")
     product = models.ForeignKey(Product, verbose_name=_(u"Product"), related_name="attachments")
-    position = models.IntegerField( _(u"Position"), default=1)
+    position = models.IntegerField(_(u"Position"), default=1)
 
     class Meta:
         ordering = ("position", )
     def get_url(self):
         if self.file.url:
             return self.file.url
-        return None
+        return None

File lfs/catalog/tests.py

 
         product = Product.objects.get(slug="product-1")
 
-        variant_data = {  'slug': 'variant-slug',
-                          'name': 'variant',
-                          'price': 10.00,
-                          }
+        variant_data = {
+            'slug': 'variant-slug',
+            'name': 'variant',
+            'price': 10.00,
+        }
 
         # set up a user with permission to access the manage interface
         self.user, created = User.objects.get_or_create(username='manager', is_superuser=True)
         )
         self.attachment_V1 = ProductAttachment.objects.create(**self.attachment_V1_data)
 
-
     def test_get_attachments(self):
         # retrieve attachments
         match_titles = [self.attachment_P1_1_data['title'],
                         self.attachment_P1_2_data['title']]
         attachments = self.p1.get_attachments()
         attachments_titles = [x.title for x in attachments]
-        self.assertEqual(match_titles,attachments_titles)
+        self.assertEqual(match_titles, attachments_titles)
 
         # check data
         first = attachments[0]
-        for k,v in self.attachment_P1_1_data.items():
-            self.assertEqual(getattr(first,k),v)
+        for k, v in self.attachment_P1_1_data.items():
+            self.assertEqual(getattr(first, k), v)
 
         second = attachments[1]
-        for k,v in self.attachment_P1_2_data.items():
-            self.assertEqual(getattr(second,k),v)
+        for k, v in self.attachment_P1_2_data.items():
+            self.assertEqual(getattr(second, k), v)
 
         # retrieve variant attachment
         attachments = self.v1.get_attachments()
         attachments_titles = [x.title for x in attachments]
-        match_titles = [self.attachment_V1_data['title'],]
-        self.assertEqual(attachments_titles,match_titles)
+        match_titles = [self.attachment_V1_data['title']]
+        self.assertEqual(attachments_titles, match_titles)
 
         # delete variant attachment: we should get parent attachments
         self.attachment_V1.delete()
         pattachments = [x.title for x in self.p1.get_attachments()]
         vattachments = [x.title for x in self.v1.get_attachments()]
-        self.assertEqual(pattachments,vattachments)
+        self.assertEqual(pattachments, vattachments)
 
         # position
         self.attachment_P1_1.position = 20
         attachments_titles = [x.title for x in self.p1.get_attachments()]
         match_titles = [self.attachment_P1_2_data['title'],
                         self.attachment_P1_1_data['title']]
-        self.assertEqual(match_titles,attachments_titles)
+        self.assertEqual(match_titles, attachments_titles)
 
 
 class ProductAccessoriesTestCase(TestCase):

File lfs/catalog/utils.py

                 }]
             continue
         else:
-            if not properties.has_key(row[0]):
+            if not row[0] in properties:
                 properties[row[0]] = []
             properties[row[0]].append({
                 "id": row[0],

File lfs/catalog/views.py

         "product_accessories": variant.get_accessories(),
         "properties": properties,
         "packing_result": packing_result,
-        "attachments" : attachments,
+        "attachments": attachments,
     }))
 
     cache.set(cache_key, result)

File lfs/core/management/commands/lfs_init.py

         ShippingMethod.objects.create(name="Standard", priority=1, active=1)
 
         # Pages
+        Page.objects.create(id=1, title="Root", slug="", active=1, exclude_from_navigation=1)
         Page.objects.create(title="Terms and Conditions", slug="terms-and-conditions", active=1, body="Enter your terms and conditions here.")
         Page.objects.create(title="Imprint", slug="imprint", active=1, body="Enter your imprint here.")
 
         # Application object
-        Application.objects.create(version="0.6")
+        Application.objects.create(version="0.7")

File lfs/core/management/commands/lfs_migrate.py

-# django importsad
+# python imports
+from copy import deepcopy
+
+# django imports
 from django.core.management.base import BaseCommand
 from django.db import connection
 from django.db import models
 
 # lfs imports
 import lfs.core.settings as lfs_settings
-from lfs.core.utils import get_default_shop
 from lfs.voucher.models import Voucher
 
 # south imports
         version = application.version
         print "Detected version: %s" % version
 
-        # 0.5 -> 0.6
         if version == "0.5":
-            print "Migrating to 0.6"
+            self.migrate_to_06(application, version)
+            self.migrate_to_07(application, version)
+            print "Your database has been migrated to version 0.7."
+        elif version == "0.6":
+            self.migrate_to_07(application, version)
+            print "Your database has been migrated to version 0.7."
+        elif version == "0.7":
+            print "You are up-to-date"
 
-            # Vouchers
-            db.add_column("voucher_voucher", "used_amount", models.PositiveSmallIntegerField(default=0))
-            db.add_column("voucher_voucher", "last_used_date", models.DateTimeField(blank=True, null=True))
-            db.add_column("voucher_voucher", "limit", models.PositiveSmallIntegerField(default=1))
+    def migrate_to_07(self, application, version):
+        from lfs.page.models import Page
 
-            for voucher in Voucher.objects.all():
-                voucher.limit = 1
-                voucher.save()
+        # Pages
+        print "Migrating to 0.7"
+        db.add_column("page_page", "meta_title", models.CharField(_(u"Meta title"), blank=True, default="<title>", max_length=80))
+        db.add_column("page_page", "meta_keywords", models.TextField(_(u"Meta keywords"), null=True, blank=True))
+        db.add_column("page_page", "meta_description", models.TextField(_(u"Meta description"), null=True, blank=True))
+        for page in Page.objects.all():
+            page.meta_title = "<title>"
+            page.meta_keywords = ""
+            page.meta_description = ""
+            page.save()
 
-            # This mus be done with execute because the old fields are not there
-            # anymore (and therefore can't be accessed via attribute) after the user
-            # has upgraded to the latest version.
-            db.execute("update voucher_voucher set used_amount = 1 where used = 1")
-            db.execute("update voucher_voucher set used_amount = 0 where used = 0")
-            db.execute("update voucher_voucher set last_used_date = used_date")
+        # Copy the old page with id=1 and create a new one with id=1, which
+        # will act as the root of all pages.
+        try:
+            page = Page.objects.get(pk=1)
+        except Page.DoesNotExist:
+            pass
+        else:
+            new_page = deepcopy(page)
+            new_page.id = None
+            new_page.save()
 
-            db.delete_column('voucher_voucher', 'used')
-            db.delete_column('voucher_voucher', 'used_date')
+            page.delete()
 
-            # Price calculator
-            db.add_column("catalog_product", "price_calculator", models.CharField(
-                null=True, blank=True, choices=lfs_settings.LFS_PRICE_CALCULATOR_DICTIONARY.items(), max_length=255))
+        Page.objects.create(id=1, title="Root", slug="", active=1, exclude_from_navigation=1)
 
-            db.add_column("core_shop", "price_calculator",
-                models.CharField(choices=lfs_settings.LFS_PRICE_CALCULATOR_DICTIONARY.items(), default="lfs.gross_price.GrossPriceCalculator", max_length=255))
+        application.version = "0.7"
+        application.save()
 
-            # Locale and currency settings
-            db.add_column("core_shop", "default_locale",
-                models.CharField(_(u"Default Shop Locale"), max_length=20, default="en_US.UTF-8"))
-            db.add_column("core_shop", "use_international_currency_code",
-                models.BooleanField(_(u"Use international currency codes"), default=False))
-            db.delete_column('core_shop', 'default_currency')
 
-            db.add_column("catalog_product", "supplier_id", models.IntegerField("Supplier", blank=True, null=True))
+    def migrate_to_06(self, application, version):
+        from lfs.core.utils import get_default_shop
 
-            # Invoice/Shipping countries
-            shop = get_default_shop()
-            db.create_table("core_shop_invoice_countries", (
-                ("id", models.AutoField(primary_key=True)),
-                ("shop_id", models.IntegerField("shop_id")),
-                ("country_id", models.IntegerField("country_id")),
-            ))
-            db.create_index("core_shop_invoice_countries", ("shop_id", ))
-            db.create_index("core_shop_invoice_countries", ("country_id", ))
-            db.create_unique("core_shop_invoice_countries", ("shop_id", "country_id"))
+        print "Migrating to 0.6"
 
-            db.create_table("core_shop_shipping_countries", (
-                ("id", models.AutoField(primary_key=True)),
-                ("shop_id", models.IntegerField("shop_id")),
-                ("country_id", models.IntegerField("country_id")),
-            ))
-            db.create_index("core_shop_shipping_countries", ("shop_id", ))
-            db.create_index("core_shop_shipping_countries", ("country_id", ))
-            db.create_unique("core_shop_shipping_countries", ("shop_id", "country_id"))
+        # Vouchers
+        db.add_column("voucher_voucher", "used_amount", models.PositiveSmallIntegerField(default=0))
+        db.add_column("voucher_voucher", "last_used_date", models.DateTimeField(blank=True, null=True))
+        db.add_column("voucher_voucher", "limit", models.PositiveSmallIntegerField(default=1))
 
-            cursor = connection.cursor()
-            cursor.execute("""SELECT country_id FROM core_shop_countries""")
-            for row in cursor.fetchall():
-                shop.invoice_countries.add(row[0])
-                shop.shipping_countries.add(row[0])
+        for voucher in Voucher.objects.all():
+            voucher.limit = 1
+            voucher.save()
 
-            db.delete_table("core_shop_countries")
+        # This mus be done with execute because the old fields are not there
+        # anymore (and therefore can't be accessed via attribute) after the user
+        # has upgraded to the latest version.
+        db.execute("update voucher_voucher set used_amount = 1 where used = 1")
+        db.execute("update voucher_voucher set used_amount = 0 where used = 0")
+        db.execute("update voucher_voucher set last_used_date = used_date")
 
-            print "Your database has been migrated to version 0.6."
+        db.delete_column('voucher_voucher', 'used')
+        db.delete_column('voucher_voucher', 'used_date')
 
-            application.version = "0.6"
-            application.save()
-        if version == "0.6":
-            print "You are up-to-date"
+        # Price calculator
+        db.add_column("catalog_product", "price_calculator", models.CharField(
+            null=True, blank=True, choices=lfs_settings.LFS_PRICE_CALCULATOR_DICTIONARY.items(), max_length=255))
+
+        db.add_column("core_shop", "price_calculator",
+            models.CharField(choices=lfs_settings.LFS_PRICE_CALCULATOR_DICTIONARY.items(), default="lfs.gross_price.GrossPriceCalculator", max_length=255))
+
+        # Locale and currency settings
+        db.add_column("core_shop", "default_locale",
+            models.CharField(_(u"Default Shop Locale"), max_length=20, default="en_US.UTF-8"))
+        db.add_column("core_shop", "use_international_currency_code",
+            models.BooleanField(_(u"Use international currency codes"), default=False))
+        db.delete_column('core_shop', 'default_currency')
+
+        db.add_column("catalog_product", "supplier_id", models.IntegerField(_(u"Supplier"), blank=True, null=True))
+
+        # Invoice/Shipping countries
+        shop = get_default_shop()
+        db.create_table("core_shop_invoice_countries", (
+            ("id", models.AutoField(primary_key=True)),
+            ("shop_id", models.IntegerField("shop_id")),
+            ("country_id", models.IntegerField("country_id")),
+        ))
+        db.create_index("core_shop_invoice_countries", ("shop_id", ))
+        db.create_index("core_shop_invoice_countries", ("country_id", ))
+        db.create_unique("core_shop_invoice_countries", ("shop_id", "country_id"))
+
+        db.create_table("core_shop_shipping_countries", (
+            ("id", models.AutoField(primary_key=True)),
+            ("shop_id", models.IntegerField("shop_id")),
+            ("country_id", models.IntegerField("country_id")),
+        ))
+        db.create_index("core_shop_shipping_countries", ("shop_id", ))
+        db.create_index("core_shop_shipping_countries", ("country_id", ))
+        db.create_unique("core_shop_shipping_countries", ("shop_id", "country_id"))
+
+        cursor = connection.cursor()
+        cursor.execute("""SELECT country_id FROM core_shop_countries""")
+        for row in cursor.fetchall():
+            shop.invoice_countries.add(row[0])
+            shop.shipping_countries.add(row[0])
+
+        db.delete_table("core_shop_countries")
+
+        application.version = "0.6"
+        application.save()

File lfs/core/templatetags/lfs_tags.py

         objects = []
         objects.append({
             "name": _(u"Information"),
-            "get_absolute_url": reverse("lfs_pages")}),
+            "url": reverse("lfs_pages")}),
         objects.append({"name": obj.title})
 
         result = {

File lfs/core/widgets/file.py

+# django imports
 from django import forms
 from django.utils.safestring import mark_safe
+from django.utils.translation import ugettext_lazy as _
 
 
 class LFSFileInput(forms.FileInput):
     """
     def render(self, name, value, attrs=None):
         output = super(LFSFileInput, self).render(name, None, attrs=attrs)
-        if value and hasattr(value, "url"):
-            output = u"""<div><a href="%s" />%s</a></div>""" % (value.url, value.name) + output
+        if value:
+            if hasattr(value, "url"):
+                output = (u"""<div><a href="%s" />%s</a></div>""" % (value.url, value.name)) + output
+            elif hasattr(value, "name"):
+                output = (u"""<div>%s</div>""" % value.name) + output
 
         if value:
             trans = _(u"Delete file")

File lfs/criteria/models/criteria.py

                 selected = False
 
             countries.append({
-                "id" : country.id,
+                "id": country.id,
                 "name": country.name,
                 "selected": selected,
             })

File lfs/customer/views.py

     else:
         now = datetime.datetime.now()
         start = now - datetime.timedelta(days=date_filter)
-        orders = orders.filter(created__gte = start)
+        orders = orders.filter(created__gte=start)
 
     options = []
     for value in [1, 3, 6, 12]:
         selected = True if value == date_filter else False
         options.append({
-            "value" : value,
-            "selected" : selected,
+            "value": value,
+            "selected": selected,
         })
 
     return render_to_response(template_name, RequestContext(request, {
         "orders": orders,
-        "options" : options,
-        "date_filter" : date_filter,
+        "options": options,
+        "date_filter": date_filter,
     }))
 
 

File lfs/manage/urls.py

     url(r'^delete-page/(?P<id>\d*)$', "delete_page", name="lfs_delete_page"),
     url(r'^manage-pages$', "manage_pages", name="lfs_manage_pages"),
     url(r'^manage-page/(?P<id>\d*)$', "manage_page", name="lfs_manage_page"),
+    url(r'^page-by-id/(?P<id>\d*)$', "page_view_by_id", name="lfs_page_view_by_id"),
     url(r'^sort-pages$', "sort_pages", name="lfs_sort_pages"),
+    url(r'^save-page-data-tab/(?P<id>\d*)$', "save_data_tab", name="lfs_save_page_data_tab"),
+    url(r'^save-page-seo-tab/(?P<id>\d*)$', "save_seo_tab", name="lfs_save_page_seo_tab"),
 )
 
 # Payment

File lfs/manage/views/lfs_portlets.py

             except PortletBlocking.DoesNotExist:
                 pass
 
-        html = portlets_inline(request, object)
-
     result = simplejson.dumps({
-        "html": html,
+        "html": [["#portlets", portlets_inline(request, object)]],
         "message": _(u"Portlet has been updated.")},
         cls=LazyEncoder
     )

File lfs/manage/views/page.py

 from django.contrib.auth.decorators import permission_required
 from django.core.urlresolvers import reverse
 from django.forms import ModelForm
+from django.http import Http404
 from django.http import HttpResponse
 from django.http import HttpResponseRedirect
 from django.shortcuts import render_to_response
 from django.shortcuts import get_object_or_404
+from django.template.loader import render_to_string
 from django.template import RequestContext
 from django.utils import simplejson
 from django.utils.translation import ugettext_lazy as _
 
 # lfs imports
 import lfs.core.utils
+from lfs.caching.utils import lfs_get_object_or_404
 from lfs.core.utils import LazyEncoder
 from lfs.core.widgets.file import LFSFileInput
+from lfs.manage.views.lfs_portlets import portlets_inline
 from lfs.page.models import Page
 
 
+# Forms
 class PageForm(ModelForm):
     """Form to edit a page.
     """
 
     class Meta:
         model = Page
-        exclude = ("position",)
+        exclude = ("position", "meta_title", "meta_description", "meta_keywords")
+
+
+class PageSEOForm(ModelForm):
+    """Form to edit page's seo data.
+    """
+    class Meta:
+        model = Page
+        fields = ("meta_title", "meta_description", "meta_keywords")
 
 
 class PageAddForm(ModelForm):
     """
     class Meta:
         model = Page
-        exclude = ("active", "position", "body", "short_text", "exclude_from_navigation", "file")
+        fields = ("title", "slug")
 
 
+# Views
 @permission_required("core.manage_shop", login_url="/login/")
 def manage_pages(request):
     """Dispatches to the first page or to the form to add a page (if there is no
     """Provides a form to edit the page with the passed id.
     """
     page = get_object_or_404(Page, pk=id)
+
+    return render_to_response(template_name, RequestContext(request, {
+        "page": page,
+        "navigation": navigation(request, page),
+        "seo_tab": seo_tab(request, page),
+        "data_tab": data_tab(request, page),
+        "portlets": portlets_inline(request, page),
+    }))
+
+
+@permission_required("core.manage_shop", login_url="/login/")
+def page_view_by_id(request, id, template_name="lfs/page/page.html"):
+    """Displays page with passed id.
+    """
+    if id == 1:
+        raise Http404()
+
+    page = lfs_get_object_or_404(Page, pk=id)
+    url = reverse("lfs_page_view", kwargs={"slug": page.slug})
+    return HttpResponseRedirect(url)
+
+
+# Parts
+def data_tab(request, page, template_name="manage/page/data_tab.html"):
+    """Renders the data tab for passed page.
+    """
     if request.method == "POST":
         form = PageForm(instance=page, data=request.POST, files=request.FILES)
         if form.is_valid():
-            new_page = form.save()
-            _update_positions()
+            page = form.save()
 
-            # delete file
-            if request.POST.get("delete_file"):
-                page.file.delete()
+        # delete file
+        if request.POST.get("delete_file"):
+            page.file.delete()
 
-            return lfs.core.utils.set_message_cookie(
-                url=reverse("lfs_manage_page", kwargs={"id": page.id}),
-                msg=_(u"Page has been saved."),
-            )
     else:
         form = PageForm(instance=page)
 
-    return render_to_response(template_name, RequestContext(request, {
+    return render_to_string(template_name, RequestContext(request, {
+        "form": form,
         "page": page,
-        "pages": Page.objects.all(),
+    }))
+
+
+def seo_tab(request, page, template_name="manage/page/seo_tab.html"):
+    """Renders the SEO tab for passed page.
+    """
+    if request.method == "POST":
+        form = PageSEOForm(instance=page, data=request.POST)
+        if form.is_valid():
+            page = form.save()
+    else:
+        form = PageSEOForm(instance=page)
+
+    return render_to_string(template_name, RequestContext(request, {
         "form": form,
-        "current_id": int(id),
+        "page": page,
     }))
 
 
+def navigation(request, page, template_name="manage/page/navigation.html"):
+    """Renders the navigation for passed page.
+    """
+    return render_to_string(template_name, RequestContext(request, {
+        "root": Page.objects.get(pk=1),
+        "page": page,
+        "pages": Page.objects.exclude(pk=1),
+    }))
+
+
+# Actions
+@permission_required("core.manage_shop", login_url="/login/")
+def save_data_tab(request, id):
+    """Saves the data tab.
+    """
+    if id == 1:
+        raise Http404()
+
+    page = lfs_get_object_or_404(Page, pk=id)
+
+    html = (
+        ("#data_tab", data_tab(request, page)),
+        ("#navigation", navigation(request, page)),
+    )
+
+    result = simplejson.dumps({
+        "html": html,
+        "message": _(u"Data has been saved."),
+    }, cls=LazyEncoder)
+
+    return HttpResponse(result)
+
+
+@permission_required("core.manage_shop", login_url="/login/")
+def save_seo_tab(request, id):
+    """Saves the seo tab.
+    """
+    if id == 1:
+        raise Http404()
+
+    page = lfs_get_object_or_404(Page, pk=id)
+
+    html = (
+        ("seo_tab", seo_tab(request, page)),
+    )
+
+    result = simplejson.dumps({
+        "html": html,
+        "message": _(u"SEO data has been saved."),
+    }, cls=LazyEncoder)
+
+    return HttpResponse(result)
+
+
 @permission_required("core.manage_shop", login_url="/login/")
 def add_page(request, template_name="manage/page/add_page.html"):
     """Provides a form to add a new page.

File lfs/manage/views/product/attachments.py

 from lfs.core.signals import product_changed
 from lfs.core.utils import LazyEncoder
 
+
 @permission_required("core.manage_shop", login_url="/login/")
 def manage_attachments(request, product_id, as_string=False, template_name="manage/product/attachments.html"):
     """
     product = lfs_get_object_or_404(Product, pk=product_id)
 
     result = render_to_string(template_name, RequestContext(request, {
-        "product" : product,
+        "product": product,
     }))
 
     if as_string:
         return result
     else:
         result = simplejson.dumps({
-            "attachments" : result,
-            "message" : _(u"Attachments have been added."),
-        }, cls = LazyEncoder)
+            "attachments": result,
+            "message": _(u"Attachments have been added."),
+        }, cls=LazyEncoder)
 
         return HttpResponse(result)
 
+
 # Actions
 @permission_required("core.manage_shop", login_url="/login/")
 def add_attachment(request, product_id):
     product_changed.send(product, request=request)
     return manage_attachments(request, product_id)
 
+
 @permission_required("core.manage_shop", login_url="/login/")
 def update_attachments(request, product_id):
     """Saves/deletes attachments with given ids (passed by request body).
 
     return HttpResponse(result)
 
+
 @permission_required("core.manage_shop", login_url="/login/")
 def move_attachment(request, id):
     """Moves the attachment with passed id up or down.

File lfs/manage/views/product/product.py

         "pages_inline": pages_inline(request, page, paginator, product_id),
         "product_data": product_data_form(request, product_id),
         "images": manage_images(request, product_id, as_string=True),
-        "attachments" : manage_attachments(request, product_id, as_string=True),
+        "attachments": manage_attachments(request, product_id, as_string=True),
         "selectable_products": selectable_products_inline(request, page, paginator, product.id),
         "seo": manage_seo(request, product_id),
         "stock": stock(request, product_id),
         result = simplejson.dumps({
             "html": html,
             "message": message,
-            "init_date": True,
         }, cls=LazyEncoder)
         return HttpResponse(result)
     else:

File lfs/manage/views/product/properties.py

                     "title": property.title,
                     "type": property.type,
                     "options": options,
-                    "value" : value,
+                    "value": value,
                     "display_text_field": not display_select_field,
                     "display_select_field": display_select_field,
                 })
                     "title": property.title,
                     "type": property.type,
                     "options": options,
-                    "value" : value,
+                    "value": value,
                     "display_text_field": not display_select_field,
                     "display_select_field": display_select_field,
                 })

File lfs/page/models.py

 from lfs.caching.utils import lfs_get_object_or_404
 from lfs.core.managers import ActiveManager
 from lfs.core.models import Shop
+from lfs.core.utils import get_default_shop
 
 
 class Page(models.Model):
     body = models.TextField(_(u"Text"), blank=True)
     file = models.FileField(_(u"File"), blank=True, upload_to="files")
 
+    meta_title = models.CharField(_(u"Meta title"), blank=True, default="<title>", max_length=80)
+    meta_keywords = models.TextField(_(u"Meta keywords"), blank=True)
+    meta_description = models.TextField(_(u"Meta description"), blank=True)
+
     objects = ActiveManager()
 
     class Meta:
     def get_absolute_url(self):
         return ("lfs_page_view", (), {"slug": self.slug})
     get_absolute_url = models.permalink(get_absolute_url)
+
+    def get_parent_for_portlets(self):
+        """Returns the parent for parents.
+        """
+        if self.id == 1:
+            return get_default_shop()
+        else:
+            return lfs_get_object_or_404(Page, pk=1)
+
+    def get_meta_title(self):
+        """Returns the meta title of the page.
+        """
+        return self.meta_title.replace("<title>", self.title)
+
+    def get_meta_keywords(self):
+        """Returns the meta keywords of the page.
+        """
+        mk = self.meta_keywords.replace("<title>", self.title)
+        return mk.replace("<short-text>", self.short_text)
+
+    def get_meta_description(self):
+        """Returns the meta description of the page.
+        """
+        md = self.meta_description.replace("<title>", self.title)
+        return md.replace("<short-text>", self.short_text)

File lfs/page/tests.py

         self.page = Page.objects.create(
             title="Page Title",
             slug="page-title",
-            body="<p>This is a body</p>"
+            body="<p>This is a body</p>",
+            short_text="This is a short text"
         )
 
     def test_add_page(self):
 
         pages = Page.objects.active()
         self.assertEqual(len(pages), 1)
+
+    def test_get_meta_title(self):
+        self.assertEqual("Page Title", self.page.get_meta_title())
+
+        self.page.meta_title = "John Doe"
+        self.page.save()
+
+        self.assertEqual("John Doe", self.page.get_meta_title())
+
+        self.page.meta_title = "<title> - John Doe"
+        self.page.save()
+
+        self.assertEqual("Page Title - John Doe", self.page.get_meta_title())
+
+        self.page.meta_title = "John Doe - <title>"
+        self.page.save()
+
+        self.assertEqual("John Doe - Page Title", self.page.get_meta_title())
+
+    def test_get_meta_keywords(self):
+        self.assertEqual("", self.page.get_meta_keywords())
+
+        self.page.meta_keywords = "John Doe"
+        self.page.save()
+
+        self.assertEqual("John Doe", self.page.get_meta_keywords())
+
+        self.page.meta_keywords = "<title> - John Doe"
+        self.page.save()
+
+        self.assertEqual("Page Title - John Doe", self.page.get_meta_keywords())
+
+        self.page.meta_keywords = "<short-text> - John Doe"
+        self.page.save()
+
+        self.assertEqual("This is a short text - John Doe", self.page.get_meta_keywords())
+
+        self.page.meta_keywords = "<short-text> - John Doe - <title>"
+        self.page.save()
+
+        self.assertEqual("This is a short text - John Doe - Page Title", self.page.get_meta_keywords())
+
+    def test_get_meta_description(self):
+        self.assertEqual("", self.page.get_meta_description())
+
+        self.page.meta_description = "John Doe"
+        self.page.save()
+
+        self.assertEqual("John Doe", self.page.get_meta_description())
+
+        self.page.meta_description = "<title> - John Doe"
+        self.page.save()
+
+        self.assertEqual("Page Title - John Doe", self.page.get_meta_description())
+
+        self.page.meta_description = "<short-text> - John Doe"
+        self.page.save()
+
+        self.assertEqual("This is a short text - John Doe", self.page.get_meta_description())
+
+        self.page.meta_description = "<short-text> - John Doe - <title>"
+        self.page.save()
+
+        self.assertEqual("This is a short text - John Doe - Page Title", self.page.get_meta_description())

File lfs/page/views.py

     """Displays page with passed slug
     """
     page = lfs_get_object_or_404(Page, slug=slug)
+    if page.id == 1:
+        raise Http404()
+
     if request.user.is_superuser or page.active:
         return render_to_response(template_name, RequestContext(request, {
             "page": page
     pages = Page.objects.filter(active=True, exclude_from_navigation=False)
 
     return render_to_response(template_name, RequestContext(request, {
-        "pages": pages
+        "pages": pages,
+        "page" : Page.objects.get(pk=1),
     }))
 
 

File lfs/portlet/templatetags/lfs_portlets_tags.py

     """
     instance = context.get("category") or \
                context.get("product") or \
+               context.get("page") or \
                lfs.core.utils.get_default_shop()
 
     cache_key = "%s-lfs-portlet-slot-%s-%s-%s" % (settings.CACHE_MIDDLEWARE_KEY_PREFIX, slot_name, instance.__class__.__name__, instance.id)

File lfs/static/css/lfs.manage.css

     padding: 2px 0;
 }
 
+.navigation-body .top-page {
+    font-size: 110%;
+}
+
 ul.manufacturer-tree {
     list-style: none;
     padding: 0;

File lfs/static/jquery/jquery.form.pack.js

-(function(b){b.fn.ajaxSubmit=function(p){if(!this.length){a("ajaxSubmit: skipping submit process - no element selected");return this}if(typeof p=="function"){p={success:p}}p=b.extend({url:this.attr("action")||window.location.toString(),type:this.attr("method")||"GET"},p||{});var s={};this.trigger("form-pre-serialize",[this,p,s]);if(s.veto){a("ajaxSubmit: submit vetoed via form-pre-serialize trigger");return this}if(p.beforeSerialize&&p.beforeSerialize(this,p)===false){a("ajaxSubmit: submit aborted via beforeSerialize callback");return this}var i=this.formToArray(p.semantic);if(p.data){p.extraData=p.data;for(var e in p.data){if(p.data[e] instanceof Array){for(var f in p.data[e]){i.push({name:e,value:p.data[e][f]})}}else{i.push({name:e,value:p.data[e]})}}}if(p.beforeSubmit&&p.beforeSubmit(i,this,p)===false){a("ajaxSubmit: submit aborted via beforeSubmit callback");return this}this.trigger("form-submit-validate",[i,this,p,s]);if(s.veto){a("ajaxSubmit: submit vetoed via form-submit-validate trigger");return this}var d=b.param(i);if(p.type.toUpperCase()=="GET"){p.url+=(p.url.indexOf("?")>=0?"&":"?")+d;p.data=null}else{p.data=d}var r=this,h=[];if(p.resetForm){h.push(function(){r.resetForm()})}if(p.clearForm){h.push(function(){r.clearForm()})}if(!p.dataType&&p.target){var m=p.success||function(){};h.push(function(j){b(p.target).html(j).each(m,arguments)})}else{if(p.success){h.push(p.success)}}p.success=function(q,k){for(var n=0,j=h.length;n<j;n++){h[n].apply(p,[q,k,r])}};var c=b("input:file",this).fieldValue();var o=false;for(var g=0;g<c.length;g++){if(c[g]){o=true}}if(p.iframe||o){if(b.browser.safari&&p.closeKeepAlive){b.get(p.closeKeepAlive,l)}else{l()}}else{b.ajax(p)}this.trigger("form-submit-notify",[this,p]);return this;function l(){var u=r[0];if(b(":input[@name=submit]",u).length){alert('Error: Form elements must not be named "submit".');return}var q=b.extend({},b.ajaxSettings,p);var D=jQuery.extend(true,{},b.extend(true,{},b.ajaxSettings),q);var t="jqFormIO"+(new Date().getTime());var z=b('<iframe id="'+t+'" name="'+t+'" />');var B=z[0];if(b.browser.msie||b.browser.opera){B.src='javascript:false;document.write("");'}z.css({position:"absolute",top:"-1000px",left:"-1000px"});var C={aborted:0,responseText:null,responseXML:null,status:0,statusText:"n/a",getAllResponseHeaders:function(){},getResponseHeader:function(){},setRequestHeader:function(){},abort:function(){this.aborted=1;z.attr("src","about:blank")}};var A=q.global;if(A&&!b.active++){b.event.trigger("ajaxStart")}if(A){b.event.trigger("ajaxSend",[C,q])}if(D.beforeSend&&D.beforeSend(C,D)===false){D.global&&jQuery.active--;return}if(C.aborted){return}var k=0;var w=0;var j=u.clk;if(j){var v=j.name;if(v&&!j.disabled){p.extraData=p.extraData||{};p.extraData[v]=j.value;if(j.type=="image"){p.extraData[name+".x"]=u.clk_x;p.extraData[name+".y"]=u.clk_y}}}setTimeout(function(){var G=r.attr("target"),E=r.attr("action");r.attr({target:t,method:"POST",action:q.url});if(!p.skipEncodingOverride){r.attr({encoding:"multipart/form-data",enctype:"multipart/form-data"})}if(q.timeout){setTimeout(function(){w=true;x()},q.timeout)}var F=[];try{if(p.extraData){for(var H in p.extraData){F.push(b('<input type="hidden" name="'+H+'" value="'+p.extraData[H]+'" />').appendTo(u)[0])}}z.appendTo("body");B.attachEvent?B.attachEvent("onload",x):B.addEventListener("load",x,false);u.submit()}finally{r.attr("action",E);G?r.attr("target",G):r.removeAttr("target");b(F).remove()}},10);function x(){if(k++){return}B.detachEvent?B.detachEvent("onload",x):B.removeEventListener("load",x,false);var E=0;var F=true;try{if(w){throw"timeout"}var G,I;I=B.contentWindow?B.contentWindow.document:B.contentDocument?B.contentDocument:B.document;if(I.body==null&&!E&&b.browser.opera){E=1;k--;setTimeout(x,100);return}C.responseText=I.body?I.body.innerHTML:null;C.responseXML=I.XMLDocument?I.XMLDocument:I;C.getResponseHeader=function(K){var J={"content-type":q.dataType};return J[K]};if(q.dataType=="json"||q.dataType=="script"){var n=I.getElementsByTagName("textarea")[0];C.responseText=n?n.value:C.responseText}else{if(q.dataType=="xml"&&!C.responseXML&&C.responseText!=null){C.responseXML=y(C.responseText)}}G=b.httpData(C,q.dataType)}catch(H){F=false;b.handleError(q,C,"error",H)}if(F){q.success(G,"success");if(A){b.event.trigger("ajaxSuccess",[C,q])}}if(A){b.event.trigger("ajaxComplete",[C,q])}if(A&&!--b.active){b.event.trigger("ajaxStop")}if(q.complete){q.complete(C,F?"success":"error")}setTimeout(function(){z.remove();C.responseXML=null},100)}function y(n,E){if(window.ActiveXObject){E=new ActiveXObject("Microsoft.XMLDOM");E.async="false";E.loadXML(n)}else{E=(new DOMParser()).parseFromString(n,"text/xml")}return(E&&E.documentElement&&E.documentElement.tagName!="parsererror")?E:null}}};b.fn.ajaxForm=function(c){return this.ajaxFormUnbind().bind("submit.form-plugin",function(){b(this).ajaxSubmit(c);return false}).each(function(){b(":submit,input:image",this).bind("click.form-plugin",function(f){var d=this.form;d.clk=this;if(this.type=="image"){if(f.offsetX!=undefined){d.clk_x=f.offsetX;d.clk_y=f.offsetY}else{if(typeof b.fn.offset=="function"){var g=b(this).offset();d.clk_x=f.pageX-g.left;d.clk_y=f.pageY-g.top}else{d.clk_x=f.pageX-this.offsetLeft;d.clk_y=f.pageY-this.offsetTop}}}setTimeout(function(){d.clk=d.clk_x=d.clk_y=null},10)})})};b.fn.ajaxFormUnbind=function(){this.unbind("submit.form-plugin");return this.each(function(){b(":submit,input:image",this).unbind("click.form-plugin")})};b.fn.formToArray=function(q){var p=[];if(this.length==0){return p}var d=this[0];var h=q?d.getElementsByTagName("*"):d.elements;if(!h){return p}for(var k=0,m=h.length;k<m;k++){var e=h[k];var f=e.name;if(!f){continue}if(q&&d.clk&&e.type=="image"){if(!e.disabled&&d.clk==e){p.push({name:f+".x",value:d.clk_x},{name:f+".y",value:d.clk_y})}continue}var r=b.fieldValue(e,true);if(r&&r.constructor==Array){for(var g=0,c=r.length;g<c;g++){p.push({name:f,value:r[g]})}}else{if(r!==null&&typeof r!="undefined"){p.push({name:f,value:r})}}}if(!q&&d.clk){var l=d.getElementsByTagName("input");for(var k=0,m=l.length;k<m;k++){var o=l[k];var f=o.name;if(f&&!o.disabled&&o.type=="image"&&d.clk==o){p.push({name:f+".x",value:d.clk_x},{name:f+".y",value:d.clk_y})}}}return p};b.fn.formSerialize=function(c){return b.param(this.formToArray(c))};b.fn.fieldSerialize=function(d){var c=[];this.each(function(){var h=this.name;if(!h){return}var f=b.fieldValue(this,d);if(f&&f.constructor==Array){for(var g=0,e=f.length;g<e;g++){c.push({name:h,value:f[g]})}}else{if(f!==null&&typeof f!="undefined"){c.push({name:this.name,value:f})}}});return b.param(c)};b.fn.fieldValue=function(h){for(var g=[],e=0,c=this.length;e<c;e++){var f=this[e];var d=b.fieldValue(f,h);if(d===null||typeof d=="undefined"||(d.constructor==Array&&!d.length)){continue}d.constructor==Array?b.merge(g,d):g.push(d)}return g};b.fieldValue=function(c,j){var e=c.name,p=c.type,q=c.tagName.toLowerCase();if(typeof j=="undefined"){j=true}if(j&&(!e||c.disabled||p=="reset"||p=="button"||(p=="checkbox"||p=="radio")&&!c.checked||(p=="submit"||p=="image")&&c.form&&c.form.clk!=c||q=="select"&&c.selectedIndex==-1)){return null}if(q=="select"){var k=c.selectedIndex;if(k<0){return null}var m=[],d=c.options;var g=(p=="select-one");var l=(g?k+1:d.length);for(var f=(g?k:0);f<l;f++){var h=d[f];if(h.selected){var o=b.browser.msie&&!(h.attributes.value.specified)?h.text:h.value;if(g){return o}m.push(o)}}return m}return c.value};b.fn.clearForm=function(){return this.each(function(){b("input,select,textarea",this).clearFields()})};b.fn.clearFields=b.fn.clearInputs=function(){return this.each(function(){var d=this.type,c=this.tagName.toLowerCase();if(d=="text"||d=="password"||c=="textarea"){this.value=""}else{if(d=="checkbox"||d=="radio"){this.checked=false}else{if(c=="select"){this.selectedIndex=-1}}}})};b.fn.resetForm=function(){return this.each(function(){if(typeof this.reset=="function"||(typeof this.reset=="object"&&!this.reset.nodeType)){this.reset()}})};b.fn.enable=function(c){if(c==undefined){c=true}return this.each(function(){this.disabled=!c})};b.fn.selected=function(c){if(c==undefined){c=true}return this.each(function(){var d=this.type;if(d=="checkbox"||d=="radio"){this.checked=c}else{if(this.tagName.toLowerCase()=="option"){var e=b(this).parent("select");if(c&&e[0]&&e[0].type=="select-one"){e.find("option").selected(false)}this.selected=c}}})};function a(){if(b.fn.ajaxSubmit.debug&&window.console&&window.console.log){window.console.log("[jquery.form] "+Array.prototype.join.call(arguments,""))}}})(jQuery);
+/*!
+ * jQuery Form Plugin
+ * version: 2.92 (22-NOV-2011)
+ * @requires jQuery v1.3.2 or later
+ *
+ * Examples and documentation at: http://malsup.com/jquery/form/
+ * Dual licensed under the MIT and GPL licenses:
+ *  http://www.opensource.org/licenses/mit-license.php
+ *  http://www.gnu.org/licenses/gpl.html
+ */
+;(function($) {
+
+/*
+    Usage Note:
+    -----------
+    Do not use both ajaxSubmit and ajaxForm on the same form.  These
+    functions are intended to be exclusive.  Use ajaxSubmit if you want
+    to bind your own submit handler to the form.  For example,
+
+    $(document).ready(function() {
+        $('#myForm').bind('submit', function(e) {
+            e.preventDefault(); // <-- important
+            $(this).ajaxSubmit({
+                target: '#output'
+            });
+        });
+    });
+
+    Use ajaxForm when you want the plugin to manage all the event binding
+    for you.  For example,
+
+    $(document).ready(function() {
+        $('#myForm').ajaxForm({
+            target: '#output'
+        });
+    });
+
+    When using ajaxForm, the ajaxSubmit function will be invoked for you
+    at the appropriate time.
+*/
+
+/**
+ * ajaxSubmit() provides a mechanism for immediately submitting
+ * an HTML form using AJAX.
+ */
+$.fn.ajaxSubmit = function(options) {
+    // fast fail if nothing selected (http://dev.jquery.com/ticket/2752)
+    if (!this.length) {
+        log('ajaxSubmit: skipping submit process - no element selected');
+        return this;
+    }
+
+    var method, action, url, $form = this;
+
+    if (typeof options == 'function') {
+        options = { success: options };
+    }
+
+    method = this.attr('method');
+    action = this.attr('action');
+    url = (typeof action === 'string') ? $.trim(action) : '';
+    url = url || window.location.href || '';
+    if (url) {
+        // clean url (don't include hash vaue)
+        url = (url.match(/^([^#]+)/)||[])[1];
+    }
+
+    options = $.extend(true, {
+        url:  url,
+        success: $.ajaxSettings.success,
+        type: method || 'GET',
+        iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank'
+    }, options);
+
+    // hook for manipulating the form data before it is extracted;
+    // convenient for use with rich editors like tinyMCE or FCKEditor
+    var veto = {};
+    this.trigger('form-pre-serialize', [this, options, veto]);
+    if (veto.veto) {
+        log('ajaxSubmit: submit vetoed via form-pre-serialize trigger');
+        return this;
+    }
+
+    // provide opportunity to alter form data before it is serialized
+    if (options.beforeSerialize && options.beforeSerialize(this, options) === false) {
+        log('ajaxSubmit: submit aborted via beforeSerialize callback');
+        return this;
+    }
+
+    var traditional = options.traditional;
+    if ( traditional === undefined ) {
+        traditional = $.ajaxSettings.traditional;
+    }
+
+    var qx,n,v,a = this.formToArray(options.semantic);
+    if (options.data) {
+        options.extraData = options.data;
+        qx = $.param(options.data, traditional);
+    }
+
+    // give pre-submit callback an opportunity to abort the submit
+    if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) {
+        log('ajaxSubmit: submit aborted via beforeSubmit callback');
+        return this;
+    }
+
+    // fire vetoable 'validate' event
+    this.trigger('form-submit-validate', [a, this, options, veto]);
+    if (veto.veto) {
+        log('ajaxSubmit: submit vetoed via form-submit-validate trigger');
+        return this;
+    }
+
+    var q = $.param(a, traditional);
+    if (qx) {
+        q = ( q ? (q + '&' + qx) : qx );
+    }
+    if (options.type.toUpperCase() == 'GET') {
+        options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q;
+        options.data = null;  // data is null for 'get'
+    }
+    else {
+        options.data = q; // data is the query string for 'post'
+    }
+
+    var callbacks = [];
+    if (options.resetForm) {
+        callbacks.push(function() { $form.resetForm(); });
+    }
+    if (options.clearForm) {
+        callbacks.push(function() { $form.clearForm(options.includeHidden); });
+    }
+
+    // perform a load on the target only if dataType is not provided
+    if (!options.dataType && options.target) {
+        var oldSuccess = options.success || function(){};
+        callbacks.push(function(data) {
+            var fn = options.replaceTarget ? 'replaceWith' : 'html';
+            $(options.target)[fn](data).each(oldSuccess, arguments);
+        });
+    }
+    else if (options.success) {
+        callbacks.push(options.success);
+    }
+
+    options.success = function(data, status, xhr) { // jQuery 1.4+ passes xhr as 3rd arg
+        var context = options.context || options;   // jQuery 1.4+ supports scope context
+        for (var i=0, max=callbacks.length; i < max; i++) {
+            callbacks[i].apply(context, [data, status, xhr || $form, $form]);
+        }
+    };
+
+    // are there files to upload?
+    var fileInputs = $('input:file:enabled[value]', this); // [value] (issue #113)
+    var hasFileInputs = fileInputs.length > 0;
+    var mp = 'multipart/form-data';
+    var multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp);
+
+    var fileAPI = !!(hasFileInputs && fileInputs.get(0).files && window.FormData);
+    log("fileAPI :" + fileAPI);
+    var shouldUseFrame = (hasFileInputs || multipart) && !fileAPI;
+
+    // options.iframe allows user to force iframe mode
+    // 06-NOV-09: now defaulting to iframe mode if file input is detected
+    if (options.iframe !== false && (options.iframe || shouldUseFrame)) {
+        // hack to fix Safari hang (thanks to Tim Molendijk for this)
+        // see:  http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d
+        if (options.closeKeepAlive) {
+            $.get(options.closeKeepAlive, function() {
+                fileUploadIframe(a);
+            });
+        }
+        else {
+            fileUploadIframe(a);
+        }
+    }
+    else if ((hasFileInputs || multipart) && fileAPI) {
+        options.progress = options.progress || $.noop;
+        fileUploadXhr(a);
+    }
+    else {
+        $.ajax(options);
+    }
+
+     // fire 'notify' event
+     this.trigger('form-submit-notify', [this, options]);
+     return this;
+
+     // XMLHttpRequest Level 2 file uploads (big hat tip to francois2metz)
+    function fileUploadXhr(a) {
+        var formdata = new FormData();
+
+        for (var i=0; i < a.length; i++) {
+            if (a[i].type == 'file')
+                continue;
+            formdata.append(a[i].name, a[i].value);
+        }
+
+        $form.find('input:file:enabled').each(function(){
+            var name = $(this).attr('name'), files = this.files;
+            if (name) {
+                for (var i=0; i < files.length; i++)
+                    formdata.append(name, files[i]);
+            }
+        });
+
+        options.data = null;
+        var _beforeSend = options.beforeSend;
+        options.beforeSend = function(xhr, options) {
+            options.data = formdata;
+            if (xhr.upload) { // unfortunately, jQuery doesn't expose this prop (http://bugs.jquery.com/ticket/10190)
+                xhr.upload.onprogress = function(event) {
+                    options.progress(event.position, event.total);
+                }
+            }
+            if (_beforeSend)
+                _beforeSend.call(options, xhr, options);
+        }
+        $.ajax(options);
+    }
+
+    // private function for handling file uploads (hat tip to YAHOO!)
+    function fileUploadIframe(a) {
+        var form = $form[0], el, i, s, g, id, $io, io, xhr, sub, n, timedOut, timeoutHandle;
+        var useProp = !!$.fn.prop;
+
+        if (a) {
+            if ( useProp ) {
+                // ensure that every serialized input is still enabled
+                for (i=0; i < a.length; i++) {
+                    el = $(form[a[i].name]);
+                    el.prop('disabled', false);
+                }
+            } else {
+                for (i=0; i < a.length; i++) {
+                    el = $(form[a[i].name]);
+                    el.removeAttr('disabled');
+                }
+            };
+        }
+
+        if ($(':input[name=submit],:input[id=submit]', form).length) {
+            // if there is an input with a name or id of 'submit' then we won't be
+            // able to invoke the submit fn on the form (at least not x-browser)
+            alert('Error: Form elements must not have name or id of "submit".');
+            return;
+        }
+
+        s = $.extend(true, {}, $.ajaxSettings, options);
+        s.context = s.context || s;
+        id = 'jqFormIO' + (new Date().getTime());
+        if (s.iframeTarget) {
+            $io = $(s.iframeTarget);
+            n = $io.attr('name');
+            if (n == null)
+                $io.attr('name', id);
+            else
+                id = n;
+        }
+        else {
+            $io = $('<iframe name="' + id + '" src="'+ s.iframeSrc +'" />');
+            $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });
+        }
+        io = $io[0];
+
+
+        xhr = { // mock object
+            aborted: 0,
+            responseText: null,
+            responseXML: null,
+            status: 0,
+            statusText: 'n/a',
+            getAllResponseHeaders: function() {},
+            getResponseHeader: function() {},
+            setRequestHeader: function() {},
+            abort: function(status) {
+                var e = (status === 'timeout' ? 'timeout' : 'aborted');
+                log('aborting upload... ' + e);
+                this.aborted = 1;
+                $io.attr('src', s.iframeSrc); // abort op in progress
+                xhr.error = e;
+                s.error && s.error.call(s.context, xhr, e, status);
+                g && $.event.trigger("ajaxError", [xhr, s, e]);
+                s.complete && s.complete.call(s.context, xhr, e);
+            }
+        };
+
+        g = s.global;
+        // trigger ajax global events so that activity/block indicators work like normal
+        if (g && ! $.active++) {
+            $.event.trigger("ajaxStart");
+        }
+        if (g) {
+            $.event.trigger("ajaxSend", [xhr, s]);
+        }
+
+        if (s.beforeSend && s.beforeSend.call(s.context, xhr, s) === false) {
+            if (s.global) {
+                $.active--;
+            }
+            return;
+        }
+        if (xhr.aborted) {
+            return;
+        }
+
+        // add submitting element to data if we know it
+        sub = form.clk;
+        if (sub) {
+            n = sub.name;
+            if (n && !sub.disabled) {
+                s.extraData = s.extraData || {};
+                s.extraData[n] = sub.value;
+                if (sub.type == "image") {
+                    s.extraData[n+'.x'] = form.clk_x;
+                    s.extraData[n+'.y'] = form.clk_y;
+                }
+            }
+        }
+
+        var CLIENT_TIMEOUT_ABORT = 1;
+        var SERVER_ABORT = 2;
+
+        function getDoc(frame) {
+            var doc = frame.contentWindow ? frame.contentWindow.document : frame.contentDocument ? frame.contentDocument : frame.document;
+            return doc;
+        }
+
+        // Rails CSRF hack (thanks to Yvan BARTHƒLEMY)
+        var csrf_token = $('meta[name=csrf-token]').attr('content');
+        var csrf_param = $('meta[name=csrf-param]').attr('content');
+        if (csrf_param && csrf_token) {
+            s.extraData = s.extraData || {};
+            s.extraData[csrf_param] = csrf_token;
+        }
+
+        // take a breath so that pending repaints get some cpu time before the upload starts
+        function doSubmit() {
+            // make sure form attrs are set
+            var t = $form.attr('target'), a = $form.attr('action');
+
+            // update form attrs in IE friendly way
+            form.setAttribute('target',id);
+            if (!method) {
+                form.setAttribute('method', 'POST');
+            }
+            if (a != s.url) {
+                form.setAttribute('action', s.url);
+            }
+
+            // ie borks in some cases when setting encoding
+            if (! s.skipEncodingOverride && (!method || /post/i.test(method))) {
+                $form.attr({
+                    encoding: 'multipart/form-data',
+                    enctype:  'multipart/form-data'
+                });
+            }
+
+            // support timout
+            if (s.timeout) {
+                timeoutHandle = setTimeout(function() { timedOut = true; cb(CLIENT_TIMEOUT_ABORT); }, s.timeout);
+            }
+
+            // look for server aborts
+            function checkState() {
+                try {
+                    var state = getDoc(io).readyState;
+                    log('state = ' + state);
+                    if (state.toLowerCase() == 'uninitialized')
+                        setTimeout(checkState,50);
+                }
+                catch(e) {
+                    log('Server abort: ' , e, ' (', e.name, ')');
+                    cb(SERVER_ABORT);
+                    timeoutHandle && clearTimeout(timeoutHandle);
+                    timeoutHandle = undefined;
+                }
+            }
+
+            // add "extra" data to form if provided in options
+            var extraInputs = [];
+            try {
+                if (s.extraData) {
+                    for (var n in s.extraData) {
+                        extraInputs.push(
+                            $('<input type="hidden" name="'+n+'">').attr('value',s.extraData[n])
+                                .appendTo(form)[0]);
+                    }
+                }
+
+                if (!s.iframeTarget) {
+                    // add iframe to doc and submit the form
+                    $io.appendTo('body');
+                    io.attachEvent ? io.attachEvent('onload', cb) : io.addEventListener('load', cb, false);
+                }
+                setTimeout(checkState,15);
+                form.submit();
+            }
+            finally {
+                // reset attrs and remove "extra" input elements
+                form.setAttribute('action',a);
+                if(t) {
+                    form.setAttribute('target', t);
+                } else {
+                    $form.removeAttr('target');
+                }
+                $(extraInputs).remove();
+            }
+        }
+
+        if (s.forceSync) {
+            doSubmit();
+        }
+        else {
+            setTimeout(doSubmit, 10); // this lets dom updates render
+        }
+
+        var data, doc, domCheckCount = 50, callbackProcessed;
+
+        function cb(e) {
+            if (xhr.aborted || callbackProcessed) {
+                return;
+            }
+            try {
+                doc = getDoc(io);
+            }
+            catch(ex) {
+                log('cannot access response document: ', ex);
+                e = SERVER_ABORT;
+            }
+            if (e === CLIENT_TIMEOUT_ABORT && xhr) {
+                xhr.abort('timeout');
+                return;
+            }
+            else if (e == SERVER_ABORT && xhr) {
+                xhr.abort('server abort');
+                return;
+            }
+
+            if (!doc || doc.location.href == s.iframeSrc) {
+                // response not received yet
+                if (!timedOut)
+                    return;
+            }
+            io.detachEvent ? io.detachEvent('onload', cb) : io.removeEventListener('load', cb, false);
+
+            var status = 'success', errMsg;
+            try {
+                if (timedOut) {
+                    throw 'timeout';
+                }
+
+                var isXml = s.dataType == 'xml' || doc.XMLDocument || $.isXMLDoc(doc);
+                log('isXml='+isXml);
+                if (!isXml && window.opera && (doc.body == null || doc.body.innerHTML == '')) {
+                    if (--domCheckCount) {
+                        // in some browsers (Opera) the iframe DOM is not always traversable when
+                        // the onload callback fires, so we loop a bit to accommodate
+                        log('requeing onLoad callback, DOM not available');
+                        setTimeout(cb, 250);
+                        return;
+                    }
+                    // let this fall through because server response could be an empty document
+                    //log('Could not access iframe DOM after mutiple tries.');
+                    //throw 'DOMException: not available';
+                }
+
+                //log('response detected');
+                var docRoot = doc.body ? doc.body : doc.documentElement;
+                xhr.responseText = docRoot ? docRoot.innerHTML : null;
+                xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;
+                if (isXml)
+                    s.dataType = 'xml';
+                xhr.getResponseHeader = function(header){
+                    var headers = {'content-type': s.dataType};
+                    return headers[header];
+                };
+                // support for XHR 'status' & 'statusText' emulation :
+                if (docRoot) {
+                    xhr.status = Number( docRoot.getAttribute('status') ) || xhr.status;
+                    xhr.statusText = docRoot.getAttribute('statusText') || xhr.statusText;
+                }
+
+                var dt = (s.dataType || '').toLowerCase();
+                var scr = /(json|script|text)/.test(dt);
+                if (scr || s.textarea) {
+                    // see if user embedded response in textarea
+                    var ta = doc.getElementsByTagName('textarea')[0];
+                    if (ta) {
+                        xhr.responseText = ta.value;
+                        // support for XHR 'status' & 'statusText' emulation :
+                        xhr.status = Number( ta.getAttribute('status') ) || xhr.status;
+                        xhr.statusText = ta.getAttribute('statusText') || xhr.statusText;
+                    }
+                    else if (scr) {
+                        // account for browsers injecting pre around json response
+                        var pre = doc.getElementsByTagName('pre')[0];
+                        var b = doc.getElementsByTagName('body')[0];
+                        if (pre) {
+                            xhr.responseText = pre.textContent ? pre.textContent : pre.innerText;
+                        }
+                        else if (b) {
+                            xhr.responseText = b.textContent ? b.textContent : b.innerText;
+                        }
+                    }
+                }
+                else if (dt == 'xml' && !xhr.responseXML && xhr.responseText != null) {
+                    xhr.responseXML = toXml(xhr.responseText);
+                }
+
+                try {
+                    data = httpData(xhr, dt, s);
+                }
+                catch (e) {
+                    status = 'parsererror';
+                    xhr.error = errMsg = (e || status);
+                }
+            }
+            catch (e) {
+                log('error caught: ',e);
+                status = 'error';
+                xhr.error = errMsg = (e || status);
+            }
+
+            if (xhr.aborted) {
+                log('upload aborted');
+                status = null;
+            }
+
+            if (xhr.status) { // we've set xhr.status
+                status = (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) ? 'success' : 'error';
+            }
+
+            // ordering of these callbacks/triggers is odd, but that's how $.ajax does it
+            if (status === 'success') {
+                s.success && s.success.call(s.context, data, 'success', xhr);
+                g && $.event.trigger("ajaxSuccess", [xhr, s]);
+            }
+            else if (status) {
+                if (errMsg == undefined)
+                    errMsg = xhr.statusText;
+                s.error && s.error.call(s.context, xhr, status, errMsg);
+                g && $.event.trigger("ajaxError", [xhr, s, errMsg]);
+            }
+
+            g && $.event.trigger("ajaxComplete", [xhr, s]);
+
+            if (g && ! --$.active) {
+                $.event.trigger("ajaxStop");
+            }
+
+            s.complete && s.complete.call(s.context, xhr, status);
+
+            callbackProcessed = true;
+            if (s.timeout)
+                clearTimeout(timeoutHandle);
+
+            // clean up
+            setTimeout(function() {
+                if (!s.iframeTarget)
+                    $io.remove();
+                xhr.responseXML = null;
+            }, 100);
+        }
+
+        var toXml = $.parseXML || function(s, doc) { // use parseXML if available (jQuery 1.5+)
+            if (window.ActiveXObject) {
+                doc = new ActiveXObject('Microsoft.XMLDOM');
+                doc.async = 'false';
+                doc.loadXML(s);
+            }
+            else {
+                doc = (new DOMParser()).parseFromString(s, 'text/xml');
+            }
+            return (doc && doc.documentElement && doc.documentElement.nodeName != 'parsererror') ? doc : null;
+        };
+        var parseJSON = $.parseJSON || function(s) {
+            return window['eval']('(' + s + ')');
+        };
+
+        var httpData = function( xhr, type, s ) { // mostly lifted from jq1.4.4
+
+            var ct = xhr.getResponseHeader('content-type') || '',
+                xml = type === 'xml' || !type && ct.indexOf('xml') >= 0,
+                data = xml ? xhr.responseXML : xhr.responseText;
+
+            if (xml && data.documentElement.nodeName === 'parsererror') {
+                $.error && $.error('parsererror');
+            }
+            if (s && s.dataFilter) {
+                data = s.dataFilter(data, type);
+            }
+            if (typeof data === 'string') {
+                if (type === 'json' || !type && ct.indexOf('json') >= 0) {
+                    data = parseJSON(data);
+                } else if (type === "script" || !type && ct.indexOf("javascript") >= 0) {
+                    $.globalEval(data);
+                }
+            }
+            return data;
+        };
+    }
+};
+
+/**
+ * ajaxForm() provides a mechanism for fully automating form submission.
+ *
+ * The advantages of using this method instead of ajaxSubmit() are:
+ *
+ * 1: This method will include coordinates for <input type="image" /> elements (if the element
+ *  is used to submit the form).
+ * 2. This method will include the submit element's name/value data (for the element that was
+ *  used to submit the form).
+ * 3. This method binds the submit() method to the form for you.
+ *
+ * The options argument for ajaxForm works exactly as it does for ajaxSubmit.  ajaxForm merely
+ * passes the options argument along after properly binding events for submit elements and
+ * the form itself.
+ */
+$.fn.ajaxForm = function(options) {
+    // in jQuery 1.3+ we can fix mistakes with the ready state
+    if (this.length === 0) {
+        var o = { s: this.selector, c: this.context };
+        if (!$.isReady && o.s) {
+            log('DOM not ready, queuing ajaxForm');
+            $(function() {
+                $(o.s,o.c).ajaxForm(options);
+            });
+            return this;
+        }
+        // is your DOM ready?  http://docs.jquery.com/Tutorials:Introducing_$(document).ready()
+        log('terminating; zero elements found by selector' + ($.isReady ? '' : ' (DOM not ready)'));
+        return this;
+    }
+
+    return this.ajaxFormUnbind().bind('submit.form-plugin', function(e) {
+        if (!e.isDefaultPrevented()) { // if event has been canceled, don't proceed
+            e.preventDefault();
+            $(this).ajaxSubmit(options);
+        }
+    }).bind('click.form-plugin', function(e) {
+        var target = e.target;
+        var $el = $(target);
+        if (!($el.is(":submit,input:image"))) {
+            // is this a child element of the submit el?  (ex: a span within a button)
+            var t = $el.closest(':submit');
+            if (t.length == 0) {
+                return;
+            }
+            target = t[0];
+        }
+        var form = this;
+        form.clk = target;
+        if (target.type == 'image') {
+            if (e.offsetX != undefined) {
+                form.clk_x = e.offsetX;
+                form.clk_y = e.offsetY;
+            } else if (typeof $.fn.offset == 'function') { // try to use dimensions plugin
+                var offset = $el.offset();
+                form.clk_x = e.pageX - offset.left;
+                form.clk_y = e.pageY - offset.top;
+            } else {
+                form.clk_x = e.pageX - target.offsetLeft;
+                form.clk_y = e.pageY - target.offsetTop;
+            }
+        }
+        // clear form vars
+        setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 100);
+    });
+};
+
+// ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm
+$.fn.ajaxFormUnbind = function() {
+    return this.unbind('submit.form-plugin click.form-plugin');
+};
+
+/**
+ * formToArray() gathers form element data into an array of objects that can
+ * be passed to any of the following ajax functions: $.get, $.post, or load.
+ * Each object in the array has both a 'name' and 'value' property.  An example of
+ * an array for a simple login form might be:
+ *
+ * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
+ *
+ * It is this array that is passed to pre-submit callback functions provided to the
+ * ajaxSubmit() and ajaxForm() methods.
+ */
+$.fn.formToArray = function(semantic) {
+    var a = [];
+    if (this.length === 0) {
+        return a;
+    }
+
+    var form = this[0];
+    var els = semantic ? form.getElementsByTagName('*') : form.elements;
+    if (!els) {
+        return a;
+    }
+
+    var i,j,n,v,el,max,jmax;
+    for(i=0, max=els.length; i < max; i++) {
+        el = els[i];
+        n = el.name;
+        if (!n) {
+            continue;
+        }
+
+        if (semantic && form.clk && el.type == "image") {
+            // handle image inputs on the fly when semantic == true
+            if(!el.disabled && form.clk == el) {
+                a.push({name: n, value: $(el).val(), type: el.type });
+                a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
+            }
+            continue;
+        }
+
+        v = $.fieldValue(el, true);
+        if (v && v.constructor == Array) {
+            for(j=0, jmax=v.length; j < jmax; j++) {
+                a.push({name: n, value: v[j]});
+            }
+        }
+        else if (v !== null && typeof v != 'undefined') {
+            a.push({name: n, value: v, type: el.type});
+        }
+    }
+
+    if (!semantic && form.clk) {
+        // input type=='image' are not found in elements array! handle it here
+        var $input = $(form.clk), input = $input[0];
+        n = input.name;
+        if (n && !input.disabled && input.type == 'image') {
+            a.push({name: n, value: $input.val()});
+            a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
+        }
+    }
+    return a;
+};
+
+/**
+ * Serializes form data into a 'submittable' string. This method will return a string
+ * in the format: name1=value1&amp;name2=value2
+ */
+$.fn.formSerialize = function(semantic) {
+    //hand off to jQuery.param for proper encoding
+    return $.param(this.formToArray(semantic));
+};
+
+/**
+ * Serializes all field elements in the jQuery object into a query string.
+ * This method will return a string in the format: name1=value1&amp;name2=value2
+ */
+$.fn.fieldSerialize = function(successful) {
+    var a = [];
+    this.each(function() {
+        var n = this.name;
+        if (!n) {
+            return;
+        }
+        var v = $.fieldValue(this, successful);
+        if (v && v.constructor == Array) {
+            for (var i=0,max=v.length; i < max; i++) {
+                a.push({name: n, value: v[i]});
+            }
+        }
+        else if (v !== null && typeof v != 'undefined') {
+            a.push({name: this.name, value: v});
+        }
+    });
+    //hand off to jQuery.param for proper encoding
+    return $.param(a);
+};
+
+/**
+ * Returns the value(s) of the element in the matched set.  For example, consider the following form:
+ *
+ *  <form><fieldset>
+ *    <input name="A" type="text" />
+ *    <input name="A" type="text" />
+ *    <input name="B" type="checkbox" value="B1" />
+ *    <input name="B" type="checkbox" value="B2"/>
+ *    <input name="C" type="radio" value="C1" />
+ *    <input name="C" type="radio" value="C2" />
+ *  </fieldset></form>
+ *
+ *  var v = $(':text').fieldValue();
+ *  // if no values are entered into the text inputs
+ *  v == ['','']
+ *  // if values entered into the text inputs are 'foo' and 'bar'
+ *  v == ['foo','bar']
+ *
+ *  var v = $(':checkbox').fieldValue();
+ *  // if neither checkbox is checked
+ *  v === undefined
+ *  // if both checkboxes are checked
+ *  v == ['B1', 'B2']
+ *
+ *  var v = $(':radio').fieldValue();
+ *  // if neither radio is checked
+ *  v === undefined
+ *  // if first radio is checked
+ *  v == ['C1']
+ *
+ * The successful argument controls whether or not the field element must be 'successful'
+ * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
+ * The default value of the successful argument is true.  If this value is false the value(s)
+ * for each element is returned.
+ *
+ * Note: This method *always* returns an array.  If no valid value can be determined the
+ *  array will be empty, otherwise it will contain one or more values.
+ */
+$.fn.fieldValue = function(successful) {
+    for (var val=[], i=0, max=this.length; i < max; i++) {
+        var el = this[i];
+        var v = $.fieldValue(el, successful);
+        if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length)) {
+            continue;
+        }
+        v.constructor == Array ? $.merge(val, v) : val.push(v);
+    }
+    return val;
+};
+
+/**
+ * Returns the value of the field element.
+ */
+$.fieldValue = function(el, successful) {
+    var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
+    if (successful === undefined) {
+        successful = true;
+    }
+
+    if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
+        (t == 'checkbox' || t == 'radio') && !el.checked ||
+        (t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
+        tag == 'select' && el.selectedIndex == -1)) {
+            return null;
+    }
+
+    if (tag == 'select') {
+        var index = el.selectedIndex;
+        if (index < 0) {
+            return null;
+        }
+        var a = [], ops = el.options;
+        var one = (t == 'select-one');
+        var max = (one ? index+1 : ops.length);
+        for(var i=(one ? index : 0); i < max; i++) {
+            var op = ops[i];
+            if (op.selected) {
+                var v = op.value;
+                if (!v) { // extra pain for IE...
+                    v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value;
+                }
+                if (one) {
+                    return v;
+                }
+                a.push(v);
+            }
+        }
+        return a;
+    }
+    return $(el).val();
+};
+
+/**
+ * Clears the form data.  Takes the following actions on the form's input fields:
+ *  - input text fields will have their 'value' property set to the empty string
+ *  - select elements will have their 'selectedIndex' property set to -1
+ *  - checkbox and radio inputs will have their 'checked' property set to false
+ *  - inputs of type submit, button, reset, and hidden will *not* be effected
+ *  - button elements will *not* be effected
+ */
+$.fn.clearForm = function(includeHidden) {
+    return this.each(function() {
+        $('input,select,textarea', this).clearFields(includeHidden);
+    });
+};
+
+/**
+ * Clears the selected form elements.
+ */
+$.fn.clearFields = $.fn.clearInputs = function(includeHidden) {
+    var re = /^(?:color|date|datetime|email|month|number|password|range|search|tel|text|time|url|week)$/i; // 'hidden' is not in this list
+    return this.each(function() {
+        var t = this.type, tag = this.tagName.toLowerCase();
+        if (re.test(t) || tag == 'textarea' || (includeHidden && /hidden/.test(t)) ) {
+            this.value = '';
+        }
+        else if (t == 'checkbox' || t == 'radio') {
+            this.checked = false;
+        }
+        else if (tag == 'select') {
+            this.selectedIndex = -1;
+        }
+    });
+};
+
+/**
+ * Resets the form data.  Causes all form elements to be reset to their original value.
+ */
+$.fn.resetForm = function() {
+    return this.each(function() {
+        // guard against an input with the name of 'reset'
+        // note that IE reports the reset function as an 'object'
+        if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType)) {
+            this.reset();
+        }
+    });
+};
+
+/**
+ * Enables or disables any matching elements.
+ */
+$.fn.enable = function(b) {
+    if (b === undefined) {
+        b = true;
+    }
+    return this.each(function() {
+        this.disabled = !b;
+    });
+};
+
+/**
+ * Checks/unchecks any matching checkboxes or radio buttons and
+ * selects/deselects and matching option elements.
+ */
+$.fn.selected = function(select) {
+    if (select === undefined) {
+        select = true;
+    }
+    return this.each(function() {
+        var t = this.type;
+        if (t == 'checkbox' || t == 'radio') {
+            this.checked = select;
+        }
+        else if (this.tagName.toLowerCase() == 'option') {
+            var $sel = $(this).parent('select');
+            if (select && $sel[0] && $sel[0].type == 'select-one') {
+                // deselect all other options
+                $sel.find('option').selected(false);
+            }
+            this.selected = select;
+        }
+    });
+};
+
+// expose debug var
+$.fn.ajaxSubmit.debug = false;
+
+// helper fn for console logging
+function log() {
+    if (!$.fn.ajaxSubmit.debug)
+        return;
+    var msg = '[jquery.form] ' + Array.prototype.join.call(arguments,'');
+    if (window.console && window.console.log) {
+        window.console.log(msg);
+    }
+    else if (window.opera && window.opera.postError) {
+        window.opera.postError(msg);
+    }
+};
+
+})(jQuery);

File lfs/static/js/lfs.manage.js

     })
 }
 
+function sortable() {
+    $('ul.sortable').sortable({
+        placeholder: 'placeholder',
+        forcePlaceholderSize: true,
+        handle: '.handle',
+        helper: 'clone',
+        items: 'li',
+        opacity: .6,
+        revert: 250,
+        tabSize: 25,
+        tolerance: 'pointer',
+        toleranceElement: '> div',
+        stop: function(event, ui){
+            var url = $(this).attr("href");
+            serialized = $('ul.sortable').sortable('serialize');
+            $.ajax({
+                url: url,
+                context: document.body,
+                type: "POST",
+                data: {"pages": serialized},
+                success: function(data) {
+                    data = $.parseJSON(data);
+                    $.jGrowl(data["message"])
+                }
+           });
+        }
+    });
+}
+
 $(function() {
     update_editor();
     $(".button").button();
                 if (data["message"]) {
                     $.jGrowl(data["message"]);
                 }
-                if (data["init_date"]) {
-                    DateTimeShortcuts.init();
-                }
                 hide_ajax_loading();
                 update_editor();
             }
         firstDay: 1
     });
 
-    $('ul.sortable').sortable({
-        placeholder: 'placeholder',
-        forcePlaceholderSize: true,
-        handle: '.handle',
-        helper: 'clone',
-        items: 'li',
-        opacity: .6,
-        revert: 250,
-        tabSize: 25,
-        tolerance: 'pointer',
-        toleranceElement: '> div',
-        stop: function(event, ui){
-            var url = $(this).attr("href");
-            serialized = $('ul.sortable').sortable('serialize');
-            $.ajax({
-                url: url,
-                context: document.body,
-                type: "POST",
-                data: {"pages": serialized},
-                success: function(data) {
-                    data = $.parseJSON(data);
-                    $.jGrowl(data["message"])
-                }
-           });
-        }
-    });
-
     $(function() {
         $('ol.sortable').nestedSortable({
             placeholder: 'placeholder',