A Cosmic Carousel of Sights and Sounds… “Sometimes in life you just got to Party”.
\r\n
Bored with the same old routine? Looking for a new night out? Then fret not folks, this could be the solution to all your problems. The Crucial party is here to save your Soul, bring you closer to your inner child, make you shake like shakira and smile like a beauty queen.We feel that it’s time for a revolution,a dancing revolution, a social revolution,a visual revolution. So we say “Let’s make Party. Lets make Good Times” and you say “Yes we can can”. Every saturday the Crucial Party will be a culture club full of eye candy and stage axe, jukebox jugglers & visual attacks,poptastical treats from 60’s to new beats. Music for Young Guns, Soul sista’s, Jazzy bro’s and Groovy twisters. But this is not just about the tunes,we feel we can touch you in places never touched before.
\r\n
We feel It just might be what you are needing. Karma For the People from under the steeple. Its a Hot-pot of Lovers Rocking, Afro Poppin’, Turbo Boogie , Slam Dunk Funk, Jezebels & Jokers, Soothsayers, Rebel city Playa’s and City Folk.
\r\n
Then it might be time to move your body to the rhythm of love and come join us at the Pav every saturday. So get on down to party down.
\r\n
\r\n
\r\n
\r\n\r\n
", "attendees": [1], "event_time": "12:06:31", "event_title": "The Crucial Party", "event_recurrence": 2, "date_created": "2012-08-15", "event_date": "2012-08-15", "event_venue": 1}}]
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 9f53a9a..aa48d97 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -13,5 +13,6 @@ django-socialauth>=0.1.2c
django-tastypie>=0.9.11
django-tinymce>=1.5.1b2
git+git://github.com/PaulUithol/backbone-tastypie.git#egg=backbone-tastypie
+git+git://github.com/maraujop/django-crispy-forms.git#django-crispy-forms
django-grappelli
humanize
\ No newline at end of file
diff --git a/spa/admin.py b/spa/admin.py
index 32a23ec..bb183f9 100644
--- a/spa/admin.py
+++ b/spa/admin.py
@@ -1,4 +1,5 @@
from django.contrib import admin
+from spa.models.Recurrence import Recurrence
from spa.models.Release import Release
from spa.models.Event import Event
from spa.models.Label import Label
@@ -20,3 +21,4 @@ admin.site.register(Release, DefaultAdmin)
admin.site.register(ReleaseAudio)
admin.site.register(Venue)
admin.site.register(Event)
+admin.site.register(Recurrence)
diff --git a/spa/api/v1/CommentResource.py b/spa/api/v1/CommentResource.py
index 59c117a..b417a84 100644
--- a/spa/api/v1/CommentResource.py
+++ b/spa/api/v1/CommentResource.py
@@ -32,5 +32,5 @@ class CommentResource(BackboneCompatibleResource):
def dehydrate(self, bundle):
bundle.data['avatar_image'] = bundle.obj.user.get_profile().get_avatar_image(150)
bundle.data['user_url'] = bundle.obj.user.get_absolute_url()
- bundle.data['user_name'] = bundle.obj.user.get_profile().nice_name()
+ bundle.data['user_name'] = bundle.obj.user.get_profile().nice_name() or bundle.obj.user.get_profile().display_name
return bundle
\ No newline at end of file
diff --git a/spa/api/v1/UserResource.py b/spa/api/v1/UserResource.py
new file mode 100644
index 0000000..4f12fb7
--- /dev/null
+++ b/spa/api/v1/UserResource.py
@@ -0,0 +1,21 @@
+from django.conf.urls import url
+from tastypie.authorization import Authorization
+from spa.api.v1.BackboneCompatibleResource import BackboneCompatibleResource
+from spa.models import UserProfile
+
+class UserResource(BackboneCompatibleResource):
+ class Meta:
+ queryset = UserProfile.objects.all()
+ authorization = Authorization()
+
+ def dehydrate(self, bundle):
+ bundle.data['display_name'] = bundle.obj.display_name
+ bundle.data['first_name'] = bundle.obj.first_name
+ bundle.data['last_name'] = bundle.obj.last_name
+ bundle.data['email'] = bundle.obj.email
+ return bundle
+
+ def override_urls(self):
+ return [
+ url(r"^(?P%s)/(?P[\w\d_.-]+)/$" % self._meta.resource_name, self.wrap_view('dispatch_detail'), name="api_dispatch_detail"),
+ ]
\ No newline at end of file
diff --git a/spa/forms.py b/spa/forms.py
new file mode 100644
index 0000000..8960d6d
--- /dev/null
+++ b/spa/forms.py
@@ -0,0 +1,73 @@
+from crispy_forms.bootstrap import FormActions
+from crispy_forms.layout import Submit, Fieldset, Layout
+from django import forms
+from django.contrib.auth.models import User
+
+from django.forms.models import ModelForm
+from crispy_forms.helper import FormHelper
+from spa.models.UserProfile import UserProfile
+
+class UserForm(ModelForm):
+ avatar_image_select = forms.ChoiceField(
+ choices=(
+ ('gravatar', "Use gravatar image."),
+ ('social', "Use Twitter/Facebook image."),
+ ('custom', "Use custom image (upload below).")
+ ),
+
+ label="Avatar Image",
+ widget=forms.RadioSelect,
+ initial='option_gravatar',
+ help_text="Select the source of your avatar image."
+ )
+ avatar_image = forms.ImageField(
+ label="",
+ required=False
+ )
+
+ class Meta:
+ model = User
+ fields = ('email', 'first_name', 'last_name')
+
+ def __init__(self, *args, **kwargs):
+ super(UserForm, self).__init__(*args, **kwargs)
+
+ self.helper = FormHelper()
+ self.helper.layout = Layout(
+ Fieldset(
+ 'User details',
+ 'display_name',
+ 'email',
+ 'first_name',
+ 'last_name',
+ 'avatar_image_select',
+ 'avatar_image'
+ ),
+ FormActions(
+ Submit('save_changes', 'Save changes', css_class="btn-primary"),
+ Submit('cancel', 'Cancel'),
+ )
+ )
+
+ self.helper.form_class = 'form-horizontal'
+ self.helper.form_id = 'id-new-user-form'
+
+ self.helper.form_method = 'post'
+ self.helper.form_error_title = 'Ooopsies'
+ self.helper.formset_error_title = 'Ooopsies'
+
+ self.helper.form_show_errors = True
+
+ def save(self, *args, **kwargs):
+ user = super(UserForm, self).save(*args, **kwargs)
+ profile = UserProfile.objects.filter(user=user)[0]
+ if profile is None:
+ profile = UserProfile()
+
+ profile.user = user
+ profile.avatar_type = self.cleaned_data['avatar_image_select']
+ profile.avatar_image = self.cleaned_data['avatar_image']
+ # and so on with the remaining fields
+ profile.save()
+ return profile
+
diff --git a/spa/models/Comment.py b/spa/models/Comment.py
index c941945..4f113aa 100644
--- a/spa/models/Comment.py
+++ b/spa/models/Comment.py
@@ -1,11 +1,10 @@
from django.contrib.auth.models import User
from django.db import models
-from spa.models.__BaseModel import __BaseModel
+from spa.models._BaseModel import _BaseModel
from spa.models.Mix import Mix
-class Comment(__BaseModel):
+class Comment(_BaseModel):
class Meta:
- db_table = 'www_comment'
app_label = 'spa'
user = models.ForeignKey(User, editable=False)
diff --git a/spa/models/Event.py b/spa/models/Event.py
index 473de62..008cd6e 100644
--- a/spa/models/Event.py
+++ b/spa/models/Event.py
@@ -2,11 +2,11 @@ from datetime import datetime
from django.contrib.auth.models import User
from django.db import models
from tinymce import models as tinymce_models
+from spa.models import Recurrence
from spa.models.Venue import Venue
class Event(models.Model):
class Meta:
- db_table = 'www_event'
app_label = 'spa'
event_venue = models.ForeignKey(Venue)
@@ -17,6 +17,7 @@ class Event(models.Model):
date_created = models.DateField(default=datetime.now())
event_title = models.CharField(max_length=250)
event_description = tinymce_models.HTMLField()
+ event_recurrence = models.ForeignKey(Recurrence)
attendees = models.ManyToManyField(User, related_name='attendees')
diff --git a/spa/models/Label.py b/spa/models/Label.py
index 1036394..5b58c56 100644
--- a/spa/models/Label.py
+++ b/spa/models/Label.py
@@ -1,9 +1,8 @@
from django.db import models
-from spa.models.__BaseModel import __BaseModel
+from spa.models._BaseModel import _BaseModel
-class Label(__BaseModel):
+class Label(_BaseModel):
class Meta:
- db_table = 'www_label'
app_label = 'spa'
name = models.CharField(max_length=100)
diff --git a/spa/models/Mix.py b/spa/models/Mix.py
index 1b77d23..9609ce9 100644
--- a/spa/models/Mix.py
+++ b/spa/models/Mix.py
@@ -5,7 +5,7 @@ import os
from core.utils.file import generate_save_file_name
from dss import settings
from spa.models.UserProfile import UserProfile
-from spa.models.__BaseModel import __BaseModel
+from spa.models._BaseModel import _BaseModel
from tasks.waveform import create_waveform_task
from django.db import models
@@ -15,9 +15,8 @@ def mix_file_name(instance, filename):
def mix_image_name(instance, filename):
return generate_save_file_name('mix-images', filename)
-class Mix(__BaseModel):
+class Mix(_BaseModel):
class Meta:
- db_table = 'www_mix'
app_label = 'spa'
title = models.CharField(max_length=50)
diff --git a/spa/models/MixLike.py b/spa/models/MixLike.py
index 0ece909..09d15b6 100644
--- a/spa/models/MixLike.py
+++ b/spa/models/MixLike.py
@@ -1,10 +1,9 @@
from django.db import models
from spa.models.Mix import Mix
-from spa.models.__Activity import __Activity
+from spa.models._Activity import _Activity
-class MixLike(__Activity):
+class MixLike(_Activity):
class Meta:
- db_table = 'www_like'
app_label = 'spa'
mix = models.ForeignKey(Mix, related_name='likes')
\ No newline at end of file
diff --git a/spa/models/MixPlay.py b/spa/models/MixPlay.py
index 40f892a..1cf1f4a 100644
--- a/spa/models/MixPlay.py
+++ b/spa/models/MixPlay.py
@@ -1,10 +1,9 @@
from django.db import models
from spa.models.Mix import Mix
-from spa.models.__Activity import __Activity
+from spa.models._Activity import _Activity
-class MixPlay(__Activity):
+class MixPlay(_Activity):
class Meta:
- db_table = 'www_play'
app_label = 'spa'
mix = models.ForeignKey(Mix, related_name='plays')
\ No newline at end of file
diff --git a/spa/models/Recurrence.py b/spa/models/Recurrence.py
new file mode 100644
index 0000000..deb6e5b
--- /dev/null
+++ b/spa/models/Recurrence.py
@@ -0,0 +1,4 @@
+from spa.models._Lookup import _Lookup
+
+class Recurrence(_Lookup):
+ pass
\ No newline at end of file
diff --git a/spa/models/Release.py b/spa/models/Release.py
index 099a83a..b0ccb66 100644
--- a/spa/models/Release.py
+++ b/spa/models/Release.py
@@ -4,7 +4,7 @@ from core.utils.file import generate_save_file_name
from dss import settings
from spa.models.Label import Label
from spa.models.UserProfile import UserProfile
-from spa.models.__BaseModel import __BaseModel
+from spa.models._BaseModel import _BaseModel
def release_image_name(instance, filename):
return generate_save_file_name('release-images', filename)
@@ -12,9 +12,8 @@ def release_image_name(instance, filename):
def release_file_name(instance, filename):
return generate_save_file_name('release-audio', filename)
-class Release(__BaseModel):
+class Release(_BaseModel):
class Meta:
- db_table = 'www_release'
app_label = 'spa'
release_artist = models.CharField(max_length=100)
@@ -44,9 +43,8 @@ class Release(__BaseModel):
return qs
-class ReleaseAudio(__BaseModel):
+class ReleaseAudio(_BaseModel):
class Meta:
- db_table = 'www_releaseaudio'
app_label = 'spa'
def __unicode__(self):
diff --git a/spa/models/UserProfile.py b/spa/models/UserProfile.py
index 1735266..68e7155 100644
--- a/spa/models/UserProfile.py
+++ b/spa/models/UserProfile.py
@@ -6,17 +6,17 @@ from django.db import models
from django.db.models.signals import post_save
from django_gravatar.helpers import has_gravatar, get_gravatar_url
from dss import settings
-from spa.models.__BaseModel import __BaseModel
+from spa.models._BaseModel import _BaseModel
-class UserProfile(__BaseModel):
+class UserProfile(_BaseModel):
class Meta:
- db_table = 'www_userprofile'
app_label = 'spa'
# This field is required.
user = models.ForeignKey(User, unique=True)
avatar_type = models.CharField(max_length=15)
avatar_image = models.ImageField(blank=True, upload_to='/avatars/')
+ display_name = models.CharField(blank=True, max_length=35)
def create_user_profile(sender, instance, created, **kwargs):
if created:
@@ -43,7 +43,7 @@ class UserProfile(__BaseModel):
return reverse('user_details', kwargs={'user_name': self.user.username})
def nice_name(self):
- return self.first_name + ' ' + self.last_name
+ return self.display_name or self.first_name + ' ' + self.last_name
def get_avatar_image(self, size=150):
avatar_type = self.avatar_type
@@ -62,4 +62,4 @@ class UserProfile(__BaseModel):
elif avatar_type == 'custom' or avatar_type:
return self.avatar_image.url
- return urlparse.urljoin(settings.STAT, "img/default-avatar-32.png")
\ No newline at end of file
+ return urlparse.urljoin(settings.STATIC_URL, "img/default-avatar-32.png")
\ No newline at end of file
diff --git a/spa/models/Venue.py b/spa/models/Venue.py
index 6704295..3597a94 100644
--- a/spa/models/Venue.py
+++ b/spa/models/Venue.py
@@ -7,7 +7,6 @@ def venue_image_name(instance, filename):
class Venue(models.Model):
class Meta:
- db_table = 'www_venue'
app_label = 'spa'
user = models.ForeignKey(User)
diff --git a/spa/models/__Activity.py b/spa/models/_Activity.py
similarity index 66%
rename from spa/models/__Activity.py
rename to spa/models/_Activity.py
index 91fb706..7cf0fea 100644
--- a/spa/models/__Activity.py
+++ b/spa/models/_Activity.py
@@ -1,7 +1,7 @@
from django.contrib.auth.models import User
from django.db import models
-from spa.models.__BaseModel import __BaseModel
+from spa.models._BaseModel import _BaseModel
-class __Activity(__BaseModel):
+class _Activity(_BaseModel):
date = models.DateTimeField(auto_now=True)
user = models.ForeignKey(User, null=True )
\ No newline at end of file
diff --git a/spa/models/__BaseModel.py b/spa/models/_BaseModel.py
similarity index 79%
rename from spa/models/__BaseModel.py
rename to spa/models/_BaseModel.py
index 8a57c27..5314080 100644
--- a/spa/models/__BaseModel.py
+++ b/spa/models/_BaseModel.py
@@ -1,7 +1,7 @@
import logging
from django.db import models
-class __BaseModel(models.Model):
+class _BaseModel(models.Model):
logger = logging.getLogger(__name__)
class Meta:
abstract = True
diff --git a/spa/models/_Lookup.py b/spa/models/_Lookup.py
new file mode 100644
index 0000000..97e0288
--- /dev/null
+++ b/spa/models/_Lookup.py
@@ -0,0 +1,8 @@
+from django.db import models
+from _BaseModel import _BaseModel
+
+class _Lookup(_BaseModel):
+ description = models.CharField(max_length=100)
+
+ def __unicode__(self):
+ return self.description
diff --git a/spa/models/__init__.py b/spa/models/__init__.py
index 8b78743..1489113 100644
--- a/spa/models/__init__.py
+++ b/spa/models/__init__.py
@@ -1,6 +1,7 @@
-from __BaseModel import __BaseModel
+from _BaseModel import _BaseModel
from UserProfile import UserProfile
-from __Activity import __Activity
+from _Activity import _Activity
+from Recurrence import Recurrence
from Comment import Comment
from Venue import Venue
from Event import Event
@@ -9,3 +10,16 @@ from Mix import Mix
from MixLike import MixLike
from MixPlay import MixPlay
from Release import Release
+
+from django.db.models import signals
+from django.contrib.auth.management import create_superuser
+from django.contrib.auth import models as auth_app
+
+# Prevent interactive question about wanting a superuser created. (This
+# code has to go in this otherwise empty "models" module so that it gets
+# processed by the "syncdb" command during database creation.)
+
+signals.post_syncdb.disconnect(
+ create_superuser,
+ sender=auth_app,
+ dispatch_uid = "django.contrib.auth.management.create_superuser")
\ No newline at end of file
diff --git a/spa/templates.py b/spa/templates.py
index 2faefea..c4d3567 100644
--- a/spa/templates.py
+++ b/spa/templates.py
@@ -1,9 +1,16 @@
from django.shortcuts import render_to_response
from django.template.context import RequestContext
+from spa.forms import UserForm
__author__ = 'fergalm'
def get_template(request, template_name):
return render_to_response(
'views/%s.html' % template_name,
- context_instance=RequestContext(request))
\ No newline at end of file
+ context_instance=RequestContext(request))
+
+def get_template_ex(request, template_name):
+ html = render_to_response(
+ 'views/%s.html' % template_name,
+ context_instance=RequestContext(request, {'form': UserForm() }))
+ return html
\ No newline at end of file
diff --git a/spa/templatetags/spa_extras.py b/spa/templatetags/spa_extras.py
index f73fc87..851a242 100644
--- a/spa/templatetags/spa_extras.py
+++ b/spa/templatetags/spa_extras.py
@@ -1,3 +1,4 @@
+import urlparse
from allauth.socialaccount.models import SocialAccount
from django import template
from django.contrib.auth.models import User
@@ -26,14 +27,14 @@ def avatar_image(user, size=150):
gravatar_exists = has_gravatar(user.email)
if gravatar_exists:
return get_gravatar_url(user.email, size)
- elif avatar_type == 'social':
+ elif avatar_type == 'social' or avatar_type == '':
try:
social_account = SocialAccount.objects.filter(user = user)[0]
if social_account:
provider = social_account.get_provider_account()
return provider.get_avatar_url()
except:
- pass
+ return urlparse.urljoin(settings.STATIC_URL, "img/default-avatar-32.png")
elif avatar_type == 'custom':
return profile.avatar_image.url
diff --git a/spa/urls.py b/spa/urls.py
index 2708ce3..fb040e5 100644
--- a/spa/urls.py
+++ b/spa/urls.py
@@ -8,6 +8,7 @@ from spa.api.v1.MixResource import MixResource
from spa.api.v1.ReleaseAudioResource import ReleaseAudioResource
from spa.api.v1.ReleaseResource import ReleaseResource
import spa
+from spa.api.v1.UserResource import UserResource
v1_api = Api(api_name='v1')
v1_api.register(MixResource())
@@ -15,12 +16,14 @@ v1_api.register(CommentResource())
v1_api.register(ReleaseResource())
v1_api.register(ReleaseAudioResource())
v1_api.register(EventResource())
+v1_api.register(UserResource())
ajax = AjaxHandler()
urlpatterns = django.conf.urls.patterns(
'',
url(r'^$', 'spa.views.app', name='home'),
url(r'^tpl/(?P\w+)/$', 'spa.templates.get_template'),
+ url(r'^tplex/(?P\w+)/$', 'spa.templates.get_template_ex'),
(r'^ajax/', include(ajax.urls)),
(r'^api/', include(v1_api.urls)),
)
\ No newline at end of file
diff --git a/static/css/style.css b/static/css/style.css
index d533762..238064e 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -8,6 +8,8 @@ body {
padding-bottom: 40px;
padding-right: 32px;
}
+/* IE/Chrome image fix */
+img.event-content {width: auto; height: auto;}
#header {
margin-top: 20px;
@@ -21,6 +23,24 @@ body {
border-bottom: 3px solid #CEC3B3;
}
+.page-header{
+ -moz-border-bottom-colors: none;
+ -moz-border-image: none;
+ -moz-border-left-colors: none;
+ -moz-border-right-colors: none;
+ -moz-border-top-colors: none;
+ border-color: -moz-use-text-color -moz-use-text-color #E5E5E5;
+ border-style: none none solid;
+ border-width: 0 0 1px;
+ color: #333333;
+ display: block;
+ font-size: 19.5px;
+ line-height: 36px;
+ margin-bottom: 27px;
+ padding: 0;
+ width: 100%;
+}
+
#mix-comments-list ul {
list-style-type: none;
}
diff --git a/static/css/sup.css b/static/css/sup.css
deleted file mode 100644
index e69de29..0000000
diff --git a/static/js/app/app.js b/static/js/app/app.js
index c127bf1..ceb072c 100644
--- a/static/js/app/app.js
+++ b/static/js/app/app.js
@@ -9,7 +9,8 @@ var AppRouter = Backbone.Router.extend({
"/events":"eventList",
"/event/:id":"eventDetails",
"/accounts/login/":"login",
- "/accounts/logout/":"logout"
+ "/accounts/logout/":"logout",
+ "/me":"userDetails"
},
initialize:function () {
this.headerView = new HeaderView();
@@ -105,11 +106,19 @@ var AppRouter = Backbone.Router.extend({
},
logout:function () {
window.utils.showAlert("Success", "You are now logged out", "alert-success");
+ },
+ userDetails: function(){
+ var user = new User();
+ $('#site-content-fill').html('');
+ user.fetch({success:function () {
+ var content = new UserView({model:user}).el;
+ $('#content').html(content);
+ }});
}
});
utils.loadTemplate([
- 'HeaderView', 'SidebarView',
+ 'HeaderView', 'SidebarView', 'UserView',
'MixListView', 'MixListItemView', 'MixView',
'CommentListView', 'CommentListItemView',
'ReleaseListView', 'ReleaseListItemView', 'ReleaseItemView', 'ReleaseView', 'ReleaseAudioListView', 'ReleaseAudioItemView',
diff --git a/static/js/app/models/user.js b/static/js/app/models/user.js
new file mode 100644
index 0000000..63e57df
--- /dev/null
+++ b/static/js/app/models/user.js
@@ -0,0 +1,3 @@
+var User = TastypieModel.extend({
+ urlRoot:window.appSettings.urlRoot + "user/"
+});
diff --git a/static/js/app/views/user.js b/static/js/app/views/user.js
new file mode 100644
index 0000000..2816bc4
--- /dev/null
+++ b/static/js/app/views/user.js
@@ -0,0 +1,25 @@
+window.UserView = Backbone.View.extend({
+ events:{
+ "click #save-changes":"saveChanges",
+ "change input" :"changed",
+ "change select" :"changed"
+ },
+ initialize:function () {
+ this.render();
+ },
+ render:function () {
+ $(this.el).html(this.template({"item":this.model.toJSON()}));
+ return this;
+ },
+ saveChanges: function(){
+ this.model.save();
+ return false;
+ },
+ changed:function(evt) {
+ var changed = evt.currentTarget;
+ var value = $("#"+changed.id).val();
+ var obj = "{\""+changed.id +"\":\""+value+"\"}";
+ var objInst = JSON.parse(obj);
+ this.model.set(objInst);
+ }
+});
\ No newline at end of file
diff --git a/static/js/libs/backbone-forms.min.js b/static/js/libs/backbone-forms.min.js
new file mode 100644
index 0000000..48d3e9f
--- /dev/null
+++ b/static/js/libs/backbone-forms.min.js
@@ -0,0 +1 @@
+(function(root){if(typeof exports!="undefined"&&typeof require!="undefined")var $=root.jQuery||root.Zepto||root.ender||require("jquery"),_=root._||require("underscore"),Backbone=root.Backbone||require("backbone");else var $=root.jQuery,_=root._,Backbone=root.Backbone;var Form=function(){return Backbone.View.extend({hasFocus:!1,initialize:function(options){if(!Form.templates.form)throw new Error("Templates not loaded");this.schema=function(){if(options.schema)return options.schema;var model=options.model;if(!model)throw new Error("Could not find schema");return _.isFunction(model.schema)?model.schema():model.schema}(),options=_.extend({template:"form",fieldsetTemplate:"fieldset",fieldTemplate:"field"},options);if(!options.fieldsets){var fields=options.fields||_.keys(this.schema);options.fieldsets=[{fields:fields}]}this.options=options,this.model=options.model,this.data=options.data,this.fields={}},render:function(){var self=this,options=this.options,template=Form.templates[options.template],$form=$(template({fieldsets:''})),$fieldsetContainer=$(".bbf-tmp",$form);return _.each(options.fieldsets,function(fieldset){$fieldsetContainer.append(self.renderFieldset(fieldset))}),$fieldsetContainer.children().unwrap(),this.setElement($form),this.hasFocus&&this.trigger("blur",this),this},renderFieldset:function(fieldset){var self=this,template=Form.templates[this.options.fieldsetTemplate],schema=this.schema,getNested=Form.helpers.getNested;_.isArray(fieldset)&&(fieldset={fields:fieldset});var $fieldset=$(template(_.extend({},fieldset,{legend:'',fields:''})));fieldset.legend?$fieldset.find(".bbf-tmp-legend").replaceWith(fieldset.legend):$fieldset.find(".bbf-tmp-legend").parent().remove();var $fieldsContainer=$(".bbf-tmp-fields",$fieldset);return _.each(fieldset.fields,function(key){var itemSchema=function(){if(schema[key])return schema[key];var path=key.replace(/\./g,".subSchema.");return getNested(schema,path)}();if(!itemSchema)throw"Field '"+key+"' not found in schema";var field=self.fields[key]=self.createField(key,itemSchema),fieldEl=field.render().el;field.editor.on("all",function(event){args=_.toArray(arguments),args[0]=key+":"+event,args.splice(1,0,this),this.trigger.apply(this,args)},self),field.editor.on("change",function(){this.trigger("change",self)},self),field.editor.on("focus",function(){if(this.hasFocus)return;this.trigger("focus",this)},self),field.editor.on("blur",function(){if(!this.hasFocus)return;var self=this;setTimeout(function(){if(_.find(self.fields,function(field){return field.editor.hasFocus}))return;self.trigger("blur",self)},0)},self),itemSchema.type!="Hidden"&&$fieldsContainer.append(fieldEl)}),$fieldsContainer=$fieldsContainer.children().unwrap(),$fieldset},createField:function(key,schema){schema.template=schema.template||this.options.fieldTemplate;var options={form:this,key:key,schema:schema,idPrefix:this.options.idPrefix,template:this.options.fieldTemplate};return this.model?options.model=this.model:this.data?options.value=this.data[key]:options.value=null,new Form.Field(options)},validate:function(){var self=this,fields=this.fields,model=this.model,errors={};_.each(fields,function(field){var error=field.validate();error&&(errors[field.key]=error)});if(model&&model.validate){var modelErrors=model.validate(this.getValue());if(modelErrors){var isDictionary=_.isObject(modelErrors)&&!_.isArray(modelErrors);isDictionary||(errors._others=errors._others||[],errors._others.push(modelErrors)),isDictionary&&_.each(modelErrors,function(val,key){if(self.fields[key]&&!errors[key])self.fields[key].setError(val);else{errors._others=errors._others||[];var tmpErr={};tmpErr[key]=val,errors._others.push(tmpErr)}})}}return _.isEmpty(errors)?null:errors},commit:function(){var errors=this.validate();if(errors)return errors;var modelError;this.model.set(this.getValue(),{error:function(model,e){modelError=e}});if(modelError)return modelError},getValue:function(key){if(key)return this.fields[key].getValue();var values={};return _.each(this.fields,function(field){values[field.key]=field.getValue()}),values},setValue:function(data){for(var key in data)this.fields[key].setValue(data[key])},focus:function(){if(this.hasFocus)return;var fieldset=this.options.fieldsets[0];if(fieldset){var field;_.isArray(fieldset)?field=fieldset[0]:field=fieldset.fields[0],field&&this.fields[field].editor.focus()}},blur:function(){if(!this.hasFocus)return;focusedField=_.find(this.fields,function(field){return field.editor.hasFocus}),focusedField&&focusedField.editor.blur()},remove:function(){var fields=this.fields;for(var key in fields)fields[key].remove();Backbone.View.prototype.remove.call(this)},trigger:function(event){return event=="focus"?this.hasFocus=!0:event=="blur"&&(this.hasFocus=!1),Backbone.View.prototype.trigger.apply(this,arguments)}})}();Form.helpers=function(){var helpers={};return helpers.getNested=function(obj,path){var fields=path.split("."),result=obj;for(var i=0,n=fields.length;i',help:''}));return $field.find(".bbf-tmp-editor").replaceWith(editor.render().el),this.$help=$(".bbf-tmp-help",$field).parent(),this.$help.empty(),this.schema.help&&this.$help.html(this.schema.help),this.schema.fieldClass&&$field.addClass(this.schema.fieldClass),this.schema.fieldAttrs&&$field.attr(this.schema.fieldAttrs),this.setElement($field),this},getId:function(){var prefix=this.options.idPrefix,id=this.key;return id=id.replace(/\./g,"_"),_.isString(prefix)||_.isNumber(prefix)?prefix+id:_.isNull(prefix)?id:this.model?this.model.cid+"_"+id:id},validate:function(){var error=this.editor.validate();return error?this.setError(error.message):this.clearError(),error},setError:function(msg){if(this.editor.hasNestedForm)return;var errClass=Form.classNames.error;this.$el.addClass(errClass),this.$help&&this.$help.html(msg)},clearError:function(){var errClass=Form.classNames.error;this.$el.removeClass(errClass);if(this.$help){this.$help.empty();var helpMsg=this.schema.help;helpMsg&&this.$help.html(helpMsg)}},commit:function(){return this.editor.commit()},getValue:function(){return this.editor.getValue()},setValue:function(value){this.editor.setValue(value)},focus:function(){this.editor.focus()},blur:function(){this.editor.blur()},remove:function(){this.editor.remove(),Backbone.View.prototype.remove.call(this)}})}(),Form.editors=function(){var helpers=Form.helpers,editors={};return editors.Base=Backbone.View.extend({defaultValue:null,hasFocus:!1,initialize:function(options){var options=options||{};if(options.model){if(!options.key)throw"Missing option: 'key'";this.model=options.model,this.value=this.model.get(options.key)}else options.value&&(this.value=options.value);this.value===undefined&&(this.value=this.defaultValue),this.key=options.key,this.form=options.form,this.schema=options.schema||{},this.validators=options.validators||this.schema.validators,this.$el.attr("name",this.getName()),this.schema.editorClass&&this.$el.addClass(this.schema.editorClass),this.schema.editorAttrs&&this.$el.attr(this.schema.editorAttrs)},getValue:function(){throw"Not implemented. Extend and override this method."},setValue:function(){throw"Not implemented. Extend and override this method."},focus:function(){throw"Not implemented. Extend and override this method."},blur:function(){throw"Not implemented. Extend and override this method."},getName:function(){var key=this.key||"";return key.replace(/\./g,"_")},commit:function(){var error=this.validate();if(error)return error;this.model.set(this.key,this.getValue(),{error:function(model,e){error=e}});if(error)return error},validate:function(){var $el=this.$el,error=null,value=this.getValue(),formValues=this.form?this.form.getValue():{},validators=this.validators,getValidator=Form.helpers.getValidator;return validators&&_.every(validators,function(validator){return error=getValidator(validator)(value,formValues),continueLoop=error?!1:!0}),error},trigger:function(event){return event=="focus"?this.hasFocus=!0:event=="blur"&&(this.hasFocus=!1),Backbone.View.prototype.trigger.apply(this,arguments)}}),editors.Text=editors.Base.extend({tagName:"input",defaultValue:"",previousValue:"",events:{keyup:"determineChange",keypress:function(event){var self=this;setTimeout(function(){self.determineChange()},0)},select:function(event){this.trigger("select",this)},focus:function(event){this.trigger("focus",this)},blur:function(event){this.trigger("blur",this)}},initialize:function(options){editors.Base.prototype.initialize.call(this,options);var schema=this.schema,type="text";schema&&schema.editorAttrs&&schema.editorAttrs.type&&(type=schema.editorAttrs.type),schema&&schema.dataType&&(type=schema.dataType),this.$el.attr("type",type)},render:function(){return this.setValue(this.value),this},determineChange:function(event){var currentValue=this.$el.val(),changed=currentValue!=this.previousValue;changed&&(this.previousValue=currentValue,this.trigger("change",this))},getValue:function(){return this.$el.val()},setValue:function(value){this.$el.val(value)},focus:function(){if(this.hasFocus)return;this.$el.focus()},blur:function(){if(!this.hasFocus)return;this.$el.blur()},select:function(){this.$el.select()}}),editors.Number=editors.Text.extend({defaultValue:0,events:_.extend({},editors.Text.prototype.events,{keypress:"onKeyPress"}),initialize:function(options){editors.Text.prototype.initialize.call(this,options),this.$el.attr("type","number")},onKeyPress:function(event){var self=this,delayedDetermineChange=function(){setTimeout(function(){self.determineChange()},0)};if(event.charCode==0){delayedDetermineChange();return}var newVal=this.$el.val()+String.fromCharCode(event.charCode),numeric=/^[0-9]*\.?[0-9]*?$/.test(newVal);numeric?delayedDetermineChange():event.preventDefault()},getValue:function(){var value=this.$el.val();return value===""?null:parseFloat(value,10)},setValue:function(value){value=function(){return _.isNumber(value)?value:_.isString(value)&&value!==""?parseFloat(value,10):null}(),_.isNaN(value)&&(value=null),editors.Text.prototype.setValue.call(this,value)}}),editors.Password=editors.Text.extend({initialize:function(options){editors.Text.prototype.initialize.call(this,options),this.$el.attr("type","password")}}),editors.TextArea=editors.Text.extend({tagName:"textarea"}),editors.Checkbox=editors.Base.extend({defaultValue:!1,tagName:"input",events:{click:function(event){this.trigger("change",this)},focus:function(event){this.trigger("focus",this)},blur:function(event){this.trigger("blur",this)}},initialize:function(options){editors.Base.prototype.initialize.call(this,options),this.$el.attr("type","checkbox")},render:function(){return this.setValue(this.value),this},getValue:function(){return this.$el.prop("checked")},setValue:function(value){value&&this.$el.prop("checked",!0)},focus:function(){if(this.hasFocus)return;this.$el.focus()},blur:function(){if(!this.hasFocus)return;this.$el.blur()}}),editors.Hidden=editors.Base.extend({defaultValue:"",initialize:function(options){editors.Text.prototype.initialize.call(this,options),this.$el.attr("type","hidden")},getValue:function(){return this.value},setValue:function(value){this.value=value},focus:function(){},blur:function(){}}),editors.Select=editors.Base.extend({tagName:"select",events:{change:function(event){this.trigger("change",this)},focus:function(event){this.trigger("focus",this)},blur:function(event){this.trigger("blur",this)}},initialize:function(options){editors.Base.prototype.initialize.call(this,options);if(!this.schema||!this.schema.options)throw"Missing required 'schema.options'"},render:function(){return this.setOptions(this.schema.options),this},setOptions:function(options){var self=this;if(options instanceof Backbone.Collection){var collection=options;collection.length>0?this.renderOptions(options):collection.fetch({success:function(collection){self.renderOptions(options)}})}else _.isFunction(options)?options(function(result){self.renderOptions(result)}):this.renderOptions(options)},renderOptions:function(options){var $select=this.$el,html;_.isString(options)?html=options:_.isArray(options)?html=this._arrayToHtml(options):options instanceof Backbone.Collection&&(html=this._collectionToHtml(options)),$select.html(html),this.setValue(this.value)},getValue:function(){return this.$el.val()},setValue:function(value){this.$el.val(value)},focus:function(){if(this.hasFocus)return;this.$el.focus()},blur:function(){if(!this.hasFocus)return;this.$el.blur()},_collectionToHtml:function(collection){var array=[];collection.each(function(model){array.push({val:model.id,label:model.toString()})});var html=this._arrayToHtml(array);return html},_arrayToHtml:function(array){var html=[];return _.each(array,function(option){if(_.isObject(option)){var val=option.val?option.val:"";html.push('")}else html.push("")}),html.join("")}}),editors.Radio=editors.Select.extend({tagName:"ul",className:"bbf-radio",events:{"click input[type=radio]:not(:checked)":function(){this.trigger("change",this)},"focus input[type=radio]":function(){if(this.hasFocus)return;this.trigger("focus",this)},"blur input[type=radio]":function(){if(!this.hasFocus)return;var self=this;setTimeout(function(){if(self.$("input[type=radio]:focus")[0])return;self.trigger("blur",self)},0)}},getValue:function(){return this.$("input[type=radio]:checked").val()},setValue:function(value){this.$("input[type=radio]").val([value])},focus:function(){if(this.hasFocus)return;var checked=this.$("input[type=radio]:checked");if(checked[0]){checked.focus();return}this.$("input[type=radio]").first().focus()},blur:function(){if(!this.hasFocus)return;this.$("input[type=radio]:focus").blur()},_arrayToHtml:function(array){var html=[],self=this;return _.each(array,function(option,index){var itemHtml="