Răsfoiți Sursa

Add new `redirect` type with admin screens to manage

Markus Ochel 12 ani în urmă
părinte
comite
e0477be95f

+ 1 - 1
TASKS.todo

@@ -1,6 +1,6 @@
 New Admin Features:
  ☐ Rename a collection
- ☐ Arbitrary site redirects
+ ✔ Arbitrary site redirects @done (13-01-05 20:22)
  ☐ Offline and crash recovery support
  ☐ Image insertion into content
  ☐ List sorting options

+ 3 - 0
admin/controllers/index.coffee

@@ -19,6 +19,7 @@ Scene       = require('models/scene')
 Block       = require('models/block')
 Contact     = require('models/contact')
 Sponsor     = require('models/sponsor')
+Redirect    = require('models/redirect')
 
 
 class App extends Spine.Controller
@@ -91,6 +92,7 @@ class App extends Spine.Controller
     Block.fetch()
     Contact.fetch()
     Sponsor.fetch()
+    Redirect.fetch()
     @dataLoaded = true
 
   unloadData: =>
@@ -104,6 +106,7 @@ class App extends Spine.Controller
     Block.deleteAll()
     Contact.deleteAll()
     Sponsor.deleteAll()
+    Redirect.deleteAll()
     @dataLoaded = false
 
   hookPanelsToNav: ->

+ 3 - 0
admin/controllers/main-stack.coffee

@@ -10,6 +10,7 @@ Scenes      = require('controllers/scenes')
 Blocks      = require('controllers/blocks')
 Contacts    = require('controllers/contacts')
 Sponsors    = require('controllers/sponsors')
+Redirects   = require('controllers/redirects')
 
 FilterBox   = require('controllers/filter-box')
 
@@ -28,6 +29,7 @@ class MainStack extends Spine.Stack
     blocks:      Blocks
     contacts:    Contacts
     sponsors:    Sponsors
+    redirects:   Redirects
 
   default: 'dashboard'
 
@@ -42,6 +44,7 @@ class MainStack extends Spine.Stack
     '/blocks':      'blocks'
     '/contacts':    'contacts'
     '/sponsors':    'sponsors'
+    '/redirects':   'redirects'
 
   constructor: ->
     super

+ 172 - 0
admin/controllers/redirects.coffee

@@ -0,0 +1,172 @@
+Spine       = require('spine/core')
+# $           = Spine.$
+templates   = require('duality/templates')
+
+Redirect    = require('models/redirect')
+Site        = require('models/site')
+
+
+class RedirectForm extends Spine.Controller
+  className: 'redirect form panel'
+
+  elements:
+    '.item-title':        'itemTitle'
+    '.error-message':     'errorMessage'
+    'form':               'form'
+    'select[name=site]':  'formSite'
+    'input[name=slug]':   'formSlug'
+    '.save-button':       'saveButton'
+    '.cancel-button':     'cancelButton'
+
+  events:
+    'submit form':            'preventSubmit'
+    'change *[name]':         'markAsDirty'
+    'keyup *[name]':          'markAsDirty'
+    'click .save-button':     'save'
+    'click .cancel-button':   'cancel'
+    'click .delete-button':   'destroy'
+    'change select[name=site]': 'siteChange'
+
+  constructor: ->
+    super
+    @active @render
+
+  render: (params) ->
+    @dirtyForm = false
+    # Get the redirect id from the url glob
+    params.id = params.match[1]
+    @editing = params.id?
+    if @editing
+      @copying = params.id.split('-')[0] is 'copy'
+      if @copying
+        @title = 'Copy Redirect'
+        @item = Redirect.find(params.id.split('-')[1]).dup()
+        # Important to indicate that we are creating a new record
+        @editing = false
+      else
+        @item = Redirect.find(params.id)
+        @title = @item.slug
+    else
+      @title = 'New Redirect'
+      @item = {}
+    
+    @item.sites = Site.all().sort(Site.nameSort)
+    @html templates.render('redirect-form.html', {}, @item)
+
+    @itemTitle.html @title
+    
+    # Set few initial form values
+    if @editing
+      @formSite.val(@item.site)
+      @formSlug.prop('readonly', true).attr('title', 'Can not change the slug')
+    else
+      @formSite.val(@stack.stack.filterBox.siteId)
+    @siteChange()
+
+  siteChange: ->
+    $siteSelected = @formSite.parents('.field').find('.site-selected')
+    site = Site.exists(@formSite.val())
+    if site
+      $siteSelected.html "<div class=\"site-name theme-#{site.theme}\">#{site.name_html}</div>"
+    else
+      $siteSelected.html ""
+
+  save: (e) ->
+    e.preventDefault()
+    if not navigator.onLine
+      alert "Can not save. You are OFFLINE."
+      return
+      
+    if @editing
+      @item.fromForm(@form)
+    else
+      @item = new Redirect().fromForm(@form)
+      @item._id = "r/#{@item.site}/#{@item.slug}"
+    
+    # Save the item and make sure it validates
+    if @item.save()
+      @back()
+    else
+      msg = @item.validate()
+      @showError msg
+
+  showError: (msg) ->
+    @errorMessage.html(msg).show()
+    @el.scrollTop(0)
+  
+  destroy: (e) ->
+    e.preventDefault()
+    if @item and confirm "Are you sure you want to delete this item?"
+      @item.destroy()
+      @back()
+
+  markAsDirty: =>
+    @dirtyForm = true
+    @saveButton.addClass('glow')
+  
+  cancel: (e) ->
+    e.preventDefault()
+    if @dirtyForm
+      if confirm "You may have some unsaved changes.\nAre you sure you want to proceed?"
+        @back()
+    else
+      @back()
+
+  back: ->
+    @navigate('/redirects/list')
+
+  preventSubmit: (e) ->
+    e.preventDefault()
+    return false
+    
+  deactivate: ->
+    @el.scrollTop(0)
+    super
+
+
+class RedirectList extends Spine.Controller
+  className: 'redirect list panel'
+
+  events:
+    'click h1 .count':    'reload'
+
+  constructor: ->
+    super
+    # @active @render
+    Redirect.bind 'change refresh', @render
+    Spine.bind 'filterbox:change', @filter
+
+  render: =>
+    context = 
+      redirects: Redirect.filter(@filterObj).sort(Redirect.nameSort)
+    @html templates.render('redirects.html', {}, context)
+
+  filter: (@filterObj) =>
+    @render()
+    @el.scrollTop(0)
+
+  reload: ->
+    Redirect.fetch()
+
+
+class Redirects extends Spine.Stack
+  className: 'redirects panel'
+
+  controllers:
+    list: RedirectList
+    form: RedirectForm
+
+  default: 'list'
+
+  routes:
+    '/redirects/list': 'list'
+    '/redirect/new':   'form'
+    '/redirect/*glob':   'form'
+
+  constructor: ->
+    super
+    for k, v of @controllers
+      @[k].active => @active()
+
+
+module.exports = Redirects

+ 1 - 1
admin/controllers/sites.coffee

@@ -58,7 +58,7 @@ class SiteForm extends Spine.Controller
     if @editing
       @formTheme.val(@item.theme)
       @formSiteId.prop('readonly', true)
-    else:
+    else
       @addSocialLink()
 
   addSocialLink: (e) ->

+ 28 - 0
admin/models/redirect.coffee

@@ -0,0 +1,28 @@
+Spine = require('spine/core')
+require('lib/spine-couch-ajax')
+
+utils = require('lib/utils')
+
+BaseModel = require('models/base')
+
+class Redirect extends BaseModel
+  @configure "Redirect", "_id", "site", "slug", "location"
+  
+  @extend @CouchAjax
+  
+  @queryOn: ['slug','location']
+    
+  validate: ->
+    return 'ID is required' unless @_id
+    return 'Site is required' unless @site
+    return 'Slug is required' unless @slug
+    return 'Location is required' unless @location
+
+    # Validate the `_id` to be unique in the system
+    if @isNew()
+      found = Redirect.exists(@_id)
+      return 'ID has been already used.' if found
+
+    return false
+
+module.exports = Redirect

+ 1 - 1
admin/templates/filter-box.html

@@ -1,4 +1,4 @@
-<input class="filter-input" type="text" value="" placeholder="Filter..." tabindex="1">
+<input class="filter-input" type="text" value="" placeholder="Keyword filter..." tabindex="1">
 <i class="icon icon-search"></i>
 <span class="clear-filter">x</span>
 <div class="selected-site">

+ 1 - 0
admin/templates/main-nav.html

@@ -17,6 +17,7 @@
   <li class="authors"><a href="#/authors"><i class="icon icon-author"></i>Authors</a></li>
   <li class="sponsors"><a href="#/sponsors"><i class="icon icon-sponsor"></i>Sponsors</a></li>
   <li class="contacts"><a href="#/contacts"><i class="icon icon-contact"></i>Contacts</a></li>
+  <li class="redirects"><a href="#/redirects"><i class="icon icon-link"></i>Redirects</a></li>
   <li class="seperator"></li>
   <li class="logout"><a class="logout-button"><i class="icon icon-off"></i>Sign Out</a></li>
 </ul>

+ 43 - 0
admin/templates/redirect-form.html

@@ -0,0 +1,43 @@
+<form class="redirect">
+  
+  <div class="content">
+    <h1>Redirect <i class="icon icon-link"></i></h1>
+    <h3 class="item-title">{{slug}}</h3>
+
+    <div class="error-message"></div>
+
+    <div class="field required">
+      <label>Site and Slug</label>
+      <select name="site" style="width: auto; display: inline-block;">
+        <option value="" disabled>Choose a site...</option>
+        {{#each sites}}
+        <option value="{{id}}">{{id}}</option>
+        {{/each}}
+      </select>
+      <div class="site-selected" style="top: 15px;"></div>
+      /
+      <input type="text" name="slug" value="{{slug}}" placeholder="some/path" style="font-weight: bold; width: 50%; min-width: 150px; display: inline-block;">
+      <div class="note">
+        No leading or trailing slashes. Slug is always from the root path.
+      </div>
+    </div>
+    <div class="field required">
+      <label>Location</label>
+      <input type="text" name="location" value="{{location}}" placeholder="/essay/some-path or http://www.example.com">
+      <div class="note">
+        Use site absolute URL with a leading slash (like <code>/collection/funny-stuff</code>, or a full URL).
+      </div>
+    </div>
+  </div>
+
+  <div class="sidebar">
+    <div class="buttons">
+      <button class="save-button" tabindex="0">Save</button>
+      <button class="cancel-button plain" tabindex="0">Cancel</button>
+      {{#if _id}}<div class="delete-button"><i class="icon-trash"></i></div>{{/if}}
+    </div>
+
+    <div class="top-spacer"></div>
+  </div>
+
+</form>

+ 24 - 0
admin/templates/redirects.html

@@ -0,0 +1,24 @@
+<div class="content">
+  <h1>
+    Redirects
+    <a class="new" href="#/redirect/new">+</a>
+    <span class="count">{{redirects.length}}</span>
+  </h1>
+  <ul class="redirects list">
+    {{#each redirects}}
+    <li>
+      <div class="actions">
+        <div>{{location}}</div>
+      </div>
+      <a class="title" href="#/redirect/{{id}}"><i class="icon icon-link"></i>{{slug}}</a>
+      <div class="meta">
+        <div><a href="http://{{site}}/{{slug}}" target="_blank">Go to {{site}}/{{slug}}</a></div>
+      </div>
+    </li>
+    {{/each}}
+  </ul>
+</div>
+
+<div class="sidebar">
+  
+</div>

+ 44 - 10
site/server/rewrites.coffee

@@ -50,18 +50,51 @@ module.exports = [
     }
   }
 
-  # Doc content page
+  # Essay content page
   {
-    from: '/render/:site/:type/:slug',
+    from: '/render/:site/essay/:slug',
     to: '_list/doc/docs_by_slug',
     query: {
-      startkey: [':site', ':type', ':slug'],
-      endkey: [':site', ':type', ':slug', {}],
+      startkey: [':site', 'essay', ':slug'],
+      endkey: [':site', 'essay', ':slug', {}],
       include_docs: 'true'
     }
   }
 
-  # Docs list for site sorted by `updated_at`
+  # Scene content page
+  {
+    from: '/render/:site/scene/:slug',
+    to: '_list/doc/docs_by_slug',
+    query: {
+      startkey: [':site', 'scene', ':slug'],
+      endkey: [':site', 'scene', ':slug', {}],
+      include_docs: 'true'
+    }
+  }
+
+  # Video content page
+  {
+    from: '/render/:site/video/:slug',
+    to: '_list/doc/docs_by_slug',
+    query: {
+      startkey: [':site', 'video', ':slug'],
+      endkey: [':site', 'video', ':slug', {}],
+      include_docs: 'true'
+    }
+  }
+
+  # Profile content page
+  {
+    from: '/render/:site/profile/:slug',
+    to: '_list/doc/docs_by_slug',
+    query: {
+      startkey: [':site', 'profile', ':slug'],
+      endkey: [':site', 'profile', ':slug', {}],
+      include_docs: 'true'
+    }
+  }
+
+  # All docs list for site sorted by `updated_at`
   {
     from: '/render/:site/docs',
     to: '_list/docs/docs_by_date',
@@ -106,11 +139,12 @@ module.exports = [
   # Redirect some direct paths
   # moved '/render/:site/some-old-path', '/some-new-path'
 
-  # For science.evolvingteachers.com old urls
-  moved '/render/:site/snc4m1-curriculum-course-material/', '/essay/snc4m1-curriculum-course-material'
-  moved '/render/:site/snc3m1-svn3m1-curriculum-course-material/', '/collection/grade-11-science'
-  moved '/render/:site/snc2d1-snc2p1-curriculum-course-material/', '/collection/grade-10-science'
-  moved '/render/:site/snc1d1-snc1p1-curriculum-course-material/', '/collection/grade-9-science'
+  # `redirect` type - from a slug to a URL
+  # doc id must be like `r/www.example.com/some-path`
+  {
+    from: '/render/:site/*',
+    to: '_show/redirect/r/:site/*'
+  }
 
   # 404 not found 
   { from: '/not-found', to: '_show/not_found' }

+ 11 - 2
site/server/shows.coffee

@@ -5,9 +5,18 @@ exports.not_found = (doc, req) ->
   title: "404 Not Found"
   content: templates.render("404.html", req, { host: req.headers.Host })
 
+exports.redirect = (doc, req) ->
+  if doc
+    code: 301
+    headers: { 'Location': doc.location }
+  else
+    code: 404
+    title: "404 Not Found"
+    content: templates.render("404.html", req, { host: req.headers.Host })
+
 exports.moved = (doc, req) ->
   code: 301
-  headers: { location: req.query.loc }
+  headers: { 'Location': req.query.loc }
 
 exports.moved_pattern = (doc, req) ->
   loc = req.query.loc
@@ -21,5 +30,5 @@ exports.moved_pattern = (doc, req) ->
   loc = loc.replace(/\:id/g, req.query.id)
   return {
     code: 301
-    headers: { location: loc }
+    headers: { 'Location': loc }
   }

+ 7 - 0
site/server/views.coffee

@@ -89,3 +89,10 @@ exports.docs_for_feeds =
         emit [doc.site, 'content', date, doc.type, doc.slug], null
       else
         emit [doc.site, 'x-other', date, doc.type, doc.slug], null
+
+
+exports.redirects_by_slug =
+  map: (doc) ->
+    if doc.site and doc.type and doc.type is 'redirect' and doc.slug and doc.location
+      emit [doc.site, doc.slug], doc.location
+