Feb 8 2010

Memcache lockless queue implementation (v2)

Jonathan

The new Google App Engine 1.3.1 SDK has an added method on memcache called “grab_tail”. This is a great little method that removes the need for my lockless queue to manage the read counter. We still need to manage the write counter though, because there doesn’t seem to be a way to add items to a memcache namespace with an arbitrary name.

I have also added a way to rollback in the case that your processing of queue items encounters an error. I was doing the reads non-destructively earlier, so I just needed to reset the read-counter to an appropriate point, but now (because grab_tail is destructive), we need to re-queue the messages.

<rant>

In this implementation, the method that gets the next write counter “__nextCounter”, is far more complicated than it should be, because of an outstanding defect on memcache. This defect is probably quite simple to fix, but since I reported it almost 6 months ago, it hasn’t even been acknowledged. I do really wonder about the value of an issue tracker to a community that doesn’t use it. If the google team are not going to use the issue tracker, it would be far better to acknowledge that and get rid of it completely.

</rant>

Here it is

from google.appengine.api import memcache
import logging
 
class Queue(object):
    itemPrefix="queueItem"
    writeCounter="writeCounter"
    def __init__(self, queueName):
        self.name = queueName
 
    def write(self, msg):
        counter = self.__nextWriteCounter()[0]
        msgKey = self.itemPrefix + str(counter)
        if not memcache.add(msgKey, msg, namespace=self.name):
            raise QueueException("msg key already existed: %s" % msgKey)
        logging.debug("wrote to %s:%s" % (self.name, msgKey))
 
    def writeMulti(self, messages):
        if len(messages) == 0:
            return
        mapping={}
        counters = self.__nextWriteCounter(len(messages))
        for msg,counter in zip(messages,counters):
            mapping[self.itemPrefix + str(counter)]=msg
 
        self.writeMulti(memcache.add_multi(mapping, namespace=self.name))
 
    def read(self):
        result = memcache.grab_tail(item_count=1, namespace=self.name)
        if len(result) > 0:
            return result[0]
        else:
            return None
 
    def readMulti(self, maxItems=100):
        return memcache.grab_tail(item_count=maxItems, namespace=self.name)
 
    def requeueMessages(self, messages):
        self.writeMulti(messages)
 
    def __currentWriteCounter(self):
        return self.__currentCounter(self.writeCounter, 0)
    def __nextWriteCounter(self,howmany=1):
        return self.__nextCounter(self.writeCounter, howmany=howmany)
 
    def __currentCounter(self, key, default):
        counter = memcache.get(key, namespace=self.name)
        if not counter:
            memcache.set(key, default, namespace=self.name)
            counter = default
        return counter
 
    def __nextCounter(self, key, howmany):
        counter = memcache.incr(key, namespace=self.name+"c", delta=howmany)
        if counter is None:
            if not memcache.add(key, howmany, namespace=self.name+"c"):
                # handles the case where another thread got in first with the add
                counter = memcache.incr(key, namespace=self.name+"c",delta=howmany)
                if counter==None:
                    raise QueueException("could not increment counter: %s" % key)
            else:
                counter = howmany
        return range(counter-(howmany-1),counter+1)
 
class QueueException(Exception):
    """Base APIProxy error type."""

and to use it:

queue=queue.Queue(clientViews.QUEUE_NAME)
def processMessages(request):
    msgs=[]
    try:
        msgs = clicksAndViewsQueue.readMulti(maxItems=100)
        if len(msgs) > 0:
            for msg in msgs:
                processMessage(msg)
 
            taskqueue.addTask(url=viewUrl, queueName=taskqueue.BACKGROUND_QUEUE)
    except Exception, e:
        clicksAndViewsQueue.requeueMessages(msgs)
        raise

Oct 14 2009

Money database property for Google App Engine

Jonathan

Calculating money is a tricky thing. Your calculations have to be ultra-precise, so no storing things as floats where $1.33 might actually be stored as 1.3299999999. That is no good for calculations… But equally it is no good storing in cents either: what is 133c /2?

Python has a Decimal datatype, but this requires serialisation to String. Nick Johnson from the Google App Engine team wrote a post about how to write a Decimal property for Google App Engine. Unfortunately this because this serialises to String, then when you do any sorting, then you get String sorting: 100 comes before 11, which comes before 20. Bummer. I played around with storing numbers with a bunch of leading zeroes ie: 0000000010. But that starts to feel a bit hacky.

So I wrote a Money database property that has 6 places of precision, and works as a normal Python numeric type. I haven’t implemented all of those methods, just the ones that I needed. I would be happy to take feedback though.

To use:

class Transaction(db.model):
    dateOccurred = db.DateProperty(auto_now_add=True)
    description = db.StringProperty(required=True)
    amount = MoneyProperty(required=True)
 
t = Transaction(description="I got some money", amount=Money(10.34))
from google.appengine.ext import db
 
class Money(object):
	multiple = 1000000.0
	def __init__(self, val, multiply=True):
		if multiply:
			self._intVal = int(float(val) * self.multiple)
		else:
			self._intVal = int(val)
 
	def format(self, places=2):
		return "%.*f" % (places, float(self))
 
	def __float__(self):
		return self._intVal / self.multiple
 
	def __repr__(self):
		return "%.06f" % (self._intVal / self.multiple)
 
	def __mul__(self, other):
		if type(other) == Money:
 			return Money((self._intVal * other._intVal) / self.multiple, False)
		else:
			return Money(self._intVal * other, False)
 
	__rmul__ = __mul__
	def __add__(self, other):
		if type(other) == Money:
 			return Money(self._intVal + other._intVal, False)
		else:
			return Money(self._intVal + (other * self.multiple), False)
 
	__radd__ = __add__
 
	def __cmp__(self, other):
		if other == None:
			return 1
		elif other == "":
			return 1
		elif type(other) != Money:
			return self._intVal - other*self.multiple
		return self._intVal - other._intVal
 
	def __sub__(self, other): 
		return Money(self._intVal - other._intVal, False)
 
	def __rsub__(self, other): 
		return Money(other._intVal - self._intVal, False)
 
	def __div__(self, other): 
		if type(other) == Money:
			return Money((self._intVal * self.multiple) / other._intVal, False)
		else:
			return Money(self._intVal / other, False)
 
	def __rdiv__(self, other): 
		if type(other) == Money:
			return Money((other._intVal * self.multiple) / self._intVal, False)
		else:
			return Money((other * self.multiple * self.multiple) / self._intVal, False)
 
	def __neg__(self): 
		return Money(self._intVal * -1, False)
 
class MoneyProperty(db.Property):
    data_type = Money
 
    def get_value_for_datastore(self, model_instance):
    	value = super(MoneyProperty, self).get_value_for_datastore(model_instance)
    	if value==None:
    		return None
    	elif isinstance(value, Money):
    		return value._intVal
    	else:
    		return Money(value)._intVal
    def make_value_from_datastore(self, value):
    	if value==None:
    		return None
    	else:
    		return Money(value, False)
 
    def empty(self, value):
    	return value == None
 
	def get_value_for_form(self, instance):
		value = super(MoneyProperty, self).get_value_for_form(instance)
		if not value:
			return None
		if isinstance(value, Money):
			return float(value)
		return value
 
	def make_value_from_form(self, value):
		if not value:
			return []
		if isinstance(value, Money):
			return Money(value)
		return value

Sep 11 2009

Google App Engine migration script (v2)

Jonathan

After talking about the previous version of my migration script, I had need to make some significant changes to it. These changes support loading model classes that do not validate in their current model version. I.E. if you have added a new required field, or have renamed a field then you can make these changes using this script.

This uses the underlying Query and Entity types, so no Model constraints are enforced. But after you make all the changes to the entity, it is then loaded into the Model class to get any defaults applied and validation.

So the main features of this migration approach is:

  • Uses the task queue for paging over all specified Model classes
  • Sets defaults from the Model definition
  • Allows adding/removing modifying of fields through a Dictionary like object (the Entity)

If you just want to apply any new defaults, you don’t need to do anything, other than specify the model class in the list to migrate. Any classes without a MigrationWorker are loaded into their Model class and have defaults and validation applied through that mechanism.

If there are specific things that you want to do to the object then you need to create your own MigrationWorker.

class ads_fooWorker(MigrationWorker):
    kind = "ads_foo"
    def processItem(self, item):
        logging.info("processing %s %s" % (item.kind(), item.key()))
        logging.info(item)
        if "views" in item and type(item['views']) == type([]):
            item['views'] = sum(item['views'])
        elif "views" not in item:
            item['views'] = 0
        if "clicks" in item and type(item['clicks']) == type([]):
            item['clicks'] = sum(item['clicks'])
        elif "clicks" not in item:
            item['clicks'] = 0
        super(ads_fooWorker, self).processItem(item)

In this class we are migrating a model class that (because we are using AEP) is called ads_foo. The underlying Entity object that we are operating on is a Dictionary like object, so we can look for keys, add keys and ‘del’ keys. As you can see here, we are changing a field from being a List of Integers to being a single Integer. This would not be possible if the object were loaded into the Model class.

The thing to be careful of (if using the taskQueue) is to make sure that you check if you have already made the changes as you are not guaranteed that your task will not be run twice. So make sure that your MigrationWorker checks if it needs to do it’s work.

The full code is here. To do my migration I usually add my MigrationWorkers directly to this script.

import logging
 
from django.http import HttpResponse
 
from google.appengine.api import datastore
from google.appengine.ext import db
from google.appengine.api.labs import taskqueue
from google.appengine.runtime import apiproxy_errors
 
"""migrate from version x to version y"""
def migrate(request):
    modelsToMigrate = ["ads_foo",
                     "ads_bar"]
    [_addTask(url=Worker.worker_url % i) for i in modelsToMigrate]
    return HttpResponse("created all tasks for processing")
 
#General Migration
def migrateModel(request, model_name):
    #create the worker class and tell it to work
    workerName = '%sWorker' % model_name
    if not workerName in globals():
        logging.info("no worker for %s" % model_name)
        Worker = generateWorker(model_name)
    else:
        Worker = globals()[workerName]
    if "start" in request.REQUEST:
        Worker(request.REQUEST['start']).work()
    else:
        Worker().work()
    return HttpResponse("ok")
 
class MigrationWorker(object):
    ITEMS_TO_FETCH = 10
    worker_url = "/worker/migrate/%s"
    def __init__(self, startKey=None):
        self.startKey = startKey
 
    def work(self):
        query = datastore.Query(self.kind)
        if self.startKey:
            query['__key__ &gt;'] = db.Key(self.startKey)
        items = query.Get(self.ITEMS_TO_FETCH)
        if not items:
            logging.info('Finished migrating %s' % self.kind)
            return
 
        last_key = items[-1].key()
        [self.processItem(x) for x in items]
 
        _addTask(url=self.worker_url % self.kind, params=dict(start=last_key))
        logging.info('Added another task to queue for %s starting at %s' %
                     (self.kind, last_key))
 
    """Override this method to do some work for each item
    """
    def processItem(self, item):
        logging.info("processing %s %s" % (item.kind(), item.key()))
        modelClass = db.class_for_kind(item.kind()).from_entity(item)
        modelClass.put()
 
def generateWorker(kind_name):
    class DynamicClass(MigrationWorker):
        kind = kind_name
    return DynamicClass
 
class ads_fooWorker(MigrationWorker):
    kind = "ads_foo"
    def processItem(self, item):
        logging.info("processing %s %s" % (item.kind(), item.key()))
        logging.info(item)
        if "views" in item and type(item['views']) == type([]):
            item['views'] = sum(item['views'])
        elif "views" not in item:
            item['views'] = 0
        if "clicks" in item and type(item['clicks']) == type([]):
            item['clicks'] = sum(item['clicks'])
        elif "clicks" not in item:
            item['clicks'] = 0
        super(ads_fooWorker, self).processItem(item)
 
def _addTask(url, params={}, queueName='default'):
    try:
        logging.info("add task to %s [%s]" % (queueName, (url, params)))
        task = taskqueue.Task(url=url, params=params)
        task.add(queueName)
    except taskqueue.TransientError, e:
        logging.exception("adding Task failed with a TransientError")
        addTask(url, params, queueName)
    except apiproxy_errors.OverQuotaError, e:
        #but keep going
        logging.exception("adding Task failed with a TransientError")

Jul 22 2009

The people around you make the difference

rob

Over at 37signals, Matt blogged on a topic recently that really resonated with me… the gist of what he said being that if a project or company is made up of a whole lot of people who don’t really know each other, individuals are generally going to play it safer than a group of people who are comfortable with one another who might fight harder to get their point heard. It doesn’t need to be a shit-fight, just an environment where people can be freely passionate and walk away as friends.  Obviously this is a generalisation and there are always people who will say what they feel – I tend to be one of them although that’s somewhat mood dependent. Anyway, the net effect of this can often be mediocrity which can be damaging or at least limiting to a project or company.

Something else that I find i’m often up against is trying to consider how important someone’s job is to them when I set expectations around quality or general awareness of what’s going on in their professional world. As someone who spends silly amounts of time working on code, reading about development / design etc at all hours of the day, I need to keep reminding myself that for many others it’s just a job and they’re happy for it to start at nine and end at five. To loosely tie this in with Matt’s point, it’s about where the line is between profession and passion and the effect of having people around that don’t necessarily care much or are indifferent to what they do.  Personally I find it frustrating and draining. Vigorous debate over things that I truly believe in (software or not) are moments that I live for, so being in situations where that can’t happen is just a little bit soul destroying. The thing with this though is that there are so many levels that you can deliver software on that are all based on the context of the business / project, cost, quality, target audience etc. There is far more work for developers than there are ‘good developers’ to do that work and the fact is that for many situations, near enough just has to be good enough. Personally, that’s just not for me though. Not that I necesarily fall ino this category (yet), but any serious product or company that excels at what they do have no time for that mentality. That’s what sets them apart.


Jun 28 2009

Google App Engine version migration

Jonathan

When writing your application on Google App Engine, you are inevitably going to deploy a version to production that does not have a final set of features. This is (of course) unavoidable. So, then for version 2, when creating new features you will also (probably) have to refactor the data model, at least adding new fields, and potentially renaming fields, or creating composed entities. So, when putting version 2 into production the data that all of your users have created in version 1 will need to be migrated to the new data-model.

Nothing exists as of now to do something similar to Rails Migrations, so everything pretty much needs to be done yourself.

The solution that I outline here is an automated way of processing over all entities in a list of Model classes. This solution loads the entities up into the v2 model classes and then calls your code to modify them (and create any new objects that might be required). This solution will not work for the case where the v2 model is missing fields that need to be read to migrate to the new state. That solution would need to be written using the underlying Entity classes and Query. (I haven’t needed to do that yet). This is really more of a recipe that can be modified to your purposes, than a generic drop-in migration tool.

The idea came from code copied from a google demo.

Here is the file: migrate.py

Directions for use (these directions are for Django, but if you are using the basic handler it would look very similar):

1. Rename migrate.py to migrateXToY.py (for your X and your Y)

2. Modify your urls.py to contain:

patterns("migrateXToY",
                       (r'^migrate/migrateXToY$', 'migrateXToY'),
                       (r'^worker/migrateXToY/(?P[^/]+)$', 'migrateModel'),
)

3. Create a migrateXToY method in the migrate.py like the following (modelsToMigrate is the list of model names that will be processed):

def migrate1To2(request):
    modelsToMigrate = ['User', 'Foo', 'Bar', 'BarDetail', 'Image', 'Report']
    [taskqueue.add(url='/worker/migrateXToY/%s' % i) for i in modelsToMigrate]
    return HttpResponse("created all tasks for processing")

4. Create a FooWorker class like the following (the migrateItem method will receive each item that is being migrated, the method should return the item after processing and should not put it. For efficiency puts are batched):

class PlacementWorker(MigrationWorker):
    kind=Foo
    kindName="Foo"
    def migrateItem(self, item):
        logging.info("processing %s %s" % (self.kindName, item.key()))
        #whatever processing you need to do
        item.cancelled=False
        return item

Jun 24 2009

Memcache lockless queue implementation

Jonathan

I had a need for an application that I am writing on Google App Engine for a way to store jobs and then process them all at once. I found the idea for a Memcached lockless queue and created an implementation of it: queue.py
To write to it (I do this from a view where I want to store some stats about the data that was shown in the view):

thisQueue=queue.Queue(QUEUE_NAME)
def method():
    thisQueue.write(data)

and then later on to read from it. I created a cron job that is executed often.

thisQueue=queue.Queue(QUEUE_NAME)
def cronMethod():
    msg = thisQueue.read()
    while msg:
        processMessage(msg)
        msg = thisQueue.read()

If you are using this on Google App Engine, you should also be careful that you don’t run out of time to execute. If you get a DeadlineExceededError after you have read, but before you have finished processing, then the message might get lost.


Jun 20 2009

jQuery Week Calendar 1.2.0 Release

rob

A new release of the week calendar plugin has been published with a number of enhancements and bug fixes. Part of this release included an internal refactoring to base the calendar off the jQuery-UI widget framework. I think this has helped to clean the code up and certainly simplified things. I think there’s still more that can be done in this area and  I expect to refine the internals over the next couple of releases. If anyone’s interested in a run down on the jQuery-UI widget structure, I found this to be a very helpful article.

This release contains the following improvements:

  • Added better layout support for overlapping of events (there are still some improvements to be made in this area but it’s a lot better than it was)
  • Added ability to supply formatters for dates and times. You can also define your own day, month name arrays to be used by the date and time formatters.
  • Added improved demo with creation / editing of events using jquery ui.
  • All demos are now served directly out of google code svn
  • Added option to only display hours defined in the ‘businessHours’ config option.
  • Migrated code-base to extend jquery-ui widget.
  • Improved inline method documentation.
  • Added ‘readonly’ config option to flag the entire calendar as readonly, preventing creation, dragging, dropping and resizing of events.
  • Added ability to configure day, month names for better i18n support
  • Fixed bug with IE7 resizing once an event gets to 2 timeslots or smaller
  • Added public method for returning an array of valid timeslots  for a given date based on the calendar options. Useful for populating select fields with start and end times.

Thanks to everyone who’s helped submitting bug reports, feature requests. I attempted to get the most pressing ones out in this release and hope to follow it up with another release in the next week or so.


May 22 2009

Flex Authorize Tag

adam

It is a very rare event when I am asked to code an application which doesn’t have some security constraints. Most Java MVC frameworks provide some type of role based security for the UI out of the box. No such thing exists in Flex. I decided, after casting about for an existing solution, to write an authorize tag for myself. This component has the following properties which need to be set:

  1. roles – one or more roles (strings) which are to be evaluated against.
  2. userRoles – one or more roles(strings) which the user has
  3. components – one or more components which will be effected if evaluation of roles against userRoles fails.
  4. type – type of evaluation to be performed one of:
    • ‘hasAny’ – evaluates true if userRole exists in any of the roles
    • ‘hasAll,’ – evaluates to true only if userRoles contains all roles
    • ‘hasNone’ – evaluates to true if userRoles does not contain any of the roles.
  5. behaviour – the behviour to be applied to the specified components:
    • Disable – disables the components if evaluation fails
    • Vanish – causes the components visible property to be set to false if evaluation fails
    • Collapse – causes the components visible and includeInLayout properties to be set to false if evaluation fails

Here is the code, please feel free to copy it and use it. Any feedback would be appreciated:

package com.express.security {
import flash.events.Event;
import flash.events.EventDispatcher;
 
import mx.collections.ArrayCollection;
import mx.collections.ICollectionView;
import mx.collections.IList;
import mx.collections.ListCollectionView;
import mx.collections.XMLListCollection;
import mx.core.UIComponent;
import mx.events.CollectionEvent;
import mx.events.FlexEvent;
 
public class Authorize extends EventDispatcher{
 
   public static const HAS_ANY : String = "hasAny";
   public static const HAS_ALL : String = "hasAll";
   public static const HAS_NONE : String = "hasNone";
 
   public static const DISABLE : String = "disable";
   public static const VANISH : String = "vanish";
   public static const COLLAPSE : String = "collapse";
   private static const BEHAVIOUR_ENUM : String = DISABLE + "," + VANISH + "," + COLLAPSE;
 
   /**
    * Roles which will be evaluated against the type rules and user's roles.
    */
   private var _roles : ICollectionView;
 
   /**
    * Roles which the current user has. These will be evauated aginst the type rules and roles.
    */
   private var _userRoles : ICollectionView;
 
   /**
    * Components which will have behaviour applied to them based on the evaluation outcome.
    */
   private var _components : ICollectionView;
 
   /**
    * Specifies the type of evaluation which will be applied to the roles
    */
   [Inspectable(enumeration="hasAny,hasAll,hasNone")]
   public var type : String;
 
   /**
    * Specifies the type of evaluation which will be applied to the roles
    */
   [Inspectable(enumeration="disable,vanish,collapse")]
   public var behaviour : String;
 
   public function Authorize() {
      super();
      addEventListener(FlexEvent.CREATION_COMPLETE, handleCreationComplete);
   }
 
   private function handleCreationComplete(event : FlexEvent) : void {
      evaluate();
   }
 
   public function evaluate() : void {
      var result : Boolean = false;
      if (_roles != null && _userRoles != null && type != null) {
         if (type == HAS_ANY) {
            result = evaluateAny();
         }
         else if (type == HAS_ALL) {
            result = evaluateAll();
         }
         else if (type == HAS_NONE) {
            result = evaluateNone();
         }
      }
      applyResult(result);
   }
 
   protected function evaluateAny() : Boolean {
      for each(var userRole : String in _userRoles) {
         if (containsRole(_roles, userRole)) {
            return true;
         }
      }
      return false;
   }
 
   protected function evaluateAll() : Boolean {
      for each(var userRole : String in _userRoles) {
         if (!containsRole(_roles, userRole)) {
            return false;
         }
      }
      return true;
   }
 
   protected function evaluateNone() : Boolean {
      for each(var userRole : String in _userRoles) {
         if (containsRole(_roles, userRole)) {
            return false;
         }
      }
      return true;
   }
 
   protected function containsRole(roles : ICollectionView, role : String) : Boolean {
      for each(var userRole : String in roles) {
         if (role == userRole) {
            return true;
         }
      }
      return false;
   }
 
   protected function applyResult(result : Boolean) : void {
      for each(var comp : UIComponent in _components) {
         switch(behaviour) {
            case DISABLE :
               comp.enabled = result;
               break;
            case VANISH :
               comp.visible = result;
               break;
            case COLLAPSE :
               comp.visible = result;
               comp.includeInLayout = result;
         }
      }
   }
 
   public function get roles():Object {
      return _roles;
   }
 
   public function set roles(val:Object):void {
      _roles = convertToCollection(val);
      evaluate();
   }
 
   public function get userRoles():Object {
      return _userRoles;
   }
 
   public function set userRoles(val:Object):void {
      _userRoles = convertToCollection(val);
      _userRoles.addEventListener(CollectionEvent.COLLECTION_CHANGE, handleCollectionChange, false, 0, true);
      evaluate();
   }
 
   public function get components():Object {
      return _components;
   }
 
   public function set components(val:Object):void {
      _components = convertToCollection(val);
      _components.addEventListener(CollectionEvent.COLLECTION_CHANGE, handleCollectionChange, false, 0, true);
      evaluate();
   }
 
   private function handleCollectionChange(event : Event) : void {
      evaluate();
   }
 
   public function convertToCollection(value : Object) : ICollectionView {
      if (value is Array) {
         return new ArrayCollection(value as Array);
      }
      else if (value is ICollectionView) {
         return ICollectionView(value);
      }
      else if (value is IList) {
         return new ListCollectionView(IList(value));
      }
      else if (value is XMLList) {
         return new XMLListCollection(value as XMLList);
      }
      else {
         // convert it to an array containing this one item
         var tmp:Array = [value];
         return new ArrayCollection(tmp);
      }
   }
}
}

May 14 2009

Simple Object Assembler release update

rob

There have been a couple of releases of the Simple Object Assembler in quick succession over the last week. These changes are all in aid of simplifying it’s use and can dramatically reduce the number of converters required in some projects due to enhancements to the auto-mapping capabilities.  One project i’m using it on saw the converters drop from 34 to 14!

A summary of the release notes from the last week are below:

0.4.1

This is a small api change release that simplifies a couple of commonly used methods. It will require changes to any existing converters. Please see upgrade notes below.

Upgrade notes:

  1. Change all converters that implement the convert(...) method to return void instead of the destination object.
  2. Change all converters that implement the alwaysIgnoreProperties() method to return an IgnoreSet.

0.4.0


May 5 2009

New jQuery Weekly Calendar Plugin

rob

After being inspired by the recent fullcalendar (month based) plugin by Adam Shaw, I decided to port a weekly calendar i’ve been working on into a proper jquery plugin. Until a couple of days ago it was practically impossible to use this calendar functionality outside of it’s host application but now it should be simple to integrate it into any jquery-ui based application.

I made a conscious decision to base the week calendar api off the fullcalendar plugin, using the same event names where it makes sense and most importantly to use an identical data format. The idea being that the two plugins could co-exist utilising an identical data source and similar programming style.

You can see the full details of the plugin on the project page but to summarise the feature set:

  • Display of calendar events within a weekly grid
  • Calendar events can be supplied as an array, url or function returning json
  • Calendar events can be dragged, dropped and resized
  • Lots of callbacks for customizing the way events are rendered plus callbacks for drag, drop, resize, mouseover, click etc
  • Automatically scrolls to current time
  • Extend the core calendar event data structure with your own data
  • Compatible with FullCalendar data sources with very similar events
jquery week calendar

jquery week calendar

At this stage there is only a basic demo in place but when time permits i’ll add a series of demos that help to show how you might extend it to fit your application requirements such as:

  • Modification of rendered events based on custom data / logic
  • Using jquery ui dialog for creating and editing events
  • Using some of the other events to provide a better experience

This plugin is an early release and i’d expect follow up revisions to be coming out very soon. Any feedback, bug reports etc would be welcome.

Check out the full documentation and demo here