From cef20d9cf3f138aaed1335dcec9e81f4d3f4f7e3 Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Mon, 11 Aug 2014 17:49:48 -0700 Subject: [PATCH 1/9] File skeleton for points store app (views, templates, urls, js files, etc) --- kalite/distributed/settings.py | 1 + kalite/distributed/urls.py | 1 + .../store/hbtemplates/store/store.handlebars | 1 + kalite/store/static/js/store/models.js | 8 +++++ kalite/store/static/js/store/views.js | 23 +++++++++++++ kalite/store/templates/store/store.html | 32 +++++++++++++++++++ kalite/store/urls.py | 9 ++++++ kalite/store/views.py | 16 +++++++++- 8 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 kalite/store/hbtemplates/store/store.handlebars create mode 100644 kalite/store/static/js/store/models.js create mode 100644 kalite/store/static/js/store/views.js create mode 100644 kalite/store/templates/store/store.html create mode 100755 kalite/store/urls.py diff --git a/kalite/distributed/settings.py b/kalite/distributed/settings.py index 7082cedf25..0620710777 100644 --- a/kalite/distributed/settings.py +++ b/kalite/distributed/settings.py @@ -55,6 +55,7 @@ def USER_FACING_PORT(): "kalite.testing", "kalite.updates", # "kalite.student_testing", + "kalite.store", "kalite.caching", "kalite.remoteadmin", # needed for remote connection "securesync", # needed for views that probe Device, Zone, even online status (BaseClient) diff --git a/kalite/distributed/urls.py b/kalite/distributed/urls.py index b0983ab99f..237dd41b7e 100644 --- a/kalite/distributed/urls.py +++ b/kalite/distributed/urls.py @@ -90,6 +90,7 @@ url(r'^exercisedashboard/$', 'exercise_dashboard', {}, 'exercise_dashboard'), url(r'^search/$', 'search', {}, 'search'), url(r'^test/', include('student_testing.urls')), + url(r'^store/', include('store.urls')), # the following pattern is a catch-all, so keep it last: # Allows remote admin of the distributed server url(r'^cryptologin/$', 'crypto_login', {}, 'crypto_login'), diff --git a/kalite/store/hbtemplates/store/store.handlebars b/kalite/store/hbtemplates/store/store.handlebars new file mode 100644 index 0000000000..dbb914d0f2 --- /dev/null +++ b/kalite/store/hbtemplates/store/store.handlebars @@ -0,0 +1 @@ +Hear ye, hear ye! Spend your points here! \ No newline at end of file diff --git a/kalite/store/static/js/store/models.js b/kalite/store/static/js/store/models.js new file mode 100644 index 0000000000..dd8221ec20 --- /dev/null +++ b/kalite/store/static/js/store/models.js @@ -0,0 +1,8 @@ +window.StoreItemModel = Backbone.Model.extend({ + +}); + +window.StoreItemCollection = Backbone.Collection.extend({ + + model: StoreItemModel +}); diff --git a/kalite/store/static/js/store/views.js b/kalite/store/static/js/store/views.js new file mode 100644 index 0000000000..2fdf912c7b --- /dev/null +++ b/kalite/store/static/js/store/views.js @@ -0,0 +1,23 @@ +window.StoreView = Backbone.View.extend({ + + template: HB.template("store/store"), + + events: { + + }, + + initialize: function() { + + _.bindAll(this); + + // this.listenTo(this.model, "change:active", this.render); + + }, + + render: function() { + this.$el.html(this.template()); // this.model.attributes + return this; + }, + +}); + diff --git a/kalite/store/templates/store/store.html b/kalite/store/templates/store/store.html new file mode 100644 index 0000000000..338f1bb6d6 --- /dev/null +++ b/kalite/store/templates/store/store.html @@ -0,0 +1,32 @@ +{% extends base_template %} + +{% load i18n %} +{% load kalite_staticfiles %} + +{% block headcss %}{{ block.super }} +{% endblock headcss %} + +{% block headjs %}{{ block.super }} + + + + + + + +{% endblock headjs %} + +{% block content %} + +
+ +{% endblock content %} diff --git a/kalite/store/urls.py b/kalite/store/urls.py new file mode 100755 index 0000000000..91025aab67 --- /dev/null +++ b/kalite/store/urls.py @@ -0,0 +1,9 @@ +from django.conf import settings +from django.conf.urls import include, patterns, url + +from . import api_urls + + +urlpatterns = patterns(__package__ + '.views', + url(r'^$', 'store', {},'store'), +) diff --git a/kalite/store/views.py b/kalite/store/views.py index 60f00ef0ef..c8a8a7d1fe 100644 --- a/kalite/store/views.py +++ b/kalite/store/views.py @@ -1 +1,15 @@ -# Create your views here. +""" +""" +from annoying.decorators import render_to +from annoying.functions import get_object_or_None + +from django.conf import settings; logging = settings.LOG +from django.contrib import messages +from django.core.urlresolvers import reverse +from django.http import HttpResponse, HttpResponseNotFound, HttpResponseRedirect, HttpResponseServerError, Http404 +from django.shortcuts import get_object_or_404 +from django.utils.translation import ugettext as _ + +@render_to("store/store.html") +def store(request): + return {} From b752eef07c4cd60ee830a469113c6cd407684d30 Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Mon, 11 Aug 2014 17:54:11 -0700 Subject: [PATCH 2/9] Remove duplicate store entry from installed_apps. --- kalite/distributed/settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/kalite/distributed/settings.py b/kalite/distributed/settings.py index 0620710777..7082cedf25 100644 --- a/kalite/distributed/settings.py +++ b/kalite/distributed/settings.py @@ -55,7 +55,6 @@ def USER_FACING_PORT(): "kalite.testing", "kalite.updates", # "kalite.student_testing", - "kalite.store", "kalite.caching", "kalite.remoteadmin", # needed for remote connection "securesync", # needed for views that probe Device, Zone, even online status (BaseClient) From bdb5f91085ceb7c165ca1119d2e55cc4e34eb665 Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Mon, 11 Aug 2014 19:48:13 -0700 Subject: [PATCH 3/9] Fleshing out templates, models and views for store interface. --- .../js/distributed/distributed-server.js | 5 + kalite/store/admin.py | 13 ++ .../store/available-store-item.handlebars | 8 + .../store/purchased-store-item.handlebars | 8 + .../store/store-wrapper.handlebars | 4 + .../store/hbtemplates/store/store.handlebars | 1 - ..._field_storetransactionlog_purchased_at.py | 138 ++++++++++++++++++ kalite/store/models.py | 1 + kalite/store/static/css/store/store.css | 0 kalite/store/static/js/store/models.js | 25 +++- kalite/store/static/js/store/views.js | 69 ++++++++- kalite/store/templates/store/store.html | 9 +- 12 files changed, 272 insertions(+), 9 deletions(-) create mode 100644 kalite/store/admin.py create mode 100644 kalite/store/hbtemplates/store/available-store-item.handlebars create mode 100644 kalite/store/hbtemplates/store/purchased-store-item.handlebars create mode 100644 kalite/store/hbtemplates/store/store-wrapper.handlebars delete mode 100644 kalite/store/hbtemplates/store/store.handlebars create mode 100644 kalite/store/migrations/0002_auto__add_field_storetransactionlog_purchased_at.py create mode 100644 kalite/store/static/css/store/store.css diff --git a/kalite/distributed/static/js/distributed/distributed-server.js b/kalite/distributed/static/js/distributed/distributed-server.js index 92db59f6f5..95a55cd936 100644 --- a/kalite/distributed/static/js/distributed/distributed-server.js +++ b/kalite/distributed/static/js/distributed/distributed-server.js @@ -96,7 +96,12 @@ var StatusModel = Backbone.Model.extend({ toggle_state("student", !self.get("is_admin") && !self.get("is_django_user") && self.get("is_logged_in")); toggle_state("admin", self.get("is_admin")); // combination of teachers & super-users }); + }, + + get_current_points: function() { + return this.get("points") + this.get("newpoints"); } + }); // create a global StatusModel instance to hold shared state, mostly as returned by the "status" api call diff --git a/kalite/store/admin.py b/kalite/store/admin.py new file mode 100644 index 0000000000..32710a4709 --- /dev/null +++ b/kalite/store/admin.py @@ -0,0 +1,13 @@ +from django.contrib import admin + +from .models import * + + +class StoreItemAdmin(admin.ModelAdmin): + list_display = ("title", "cost",) +admin.site.register(StoreItem, StoreItemAdmin) + + +class StoreTransactionLogAdmin(admin.ModelAdmin): + list_display = ("item", "user", "purchased_at") +admin.site.register(StoreTransactionLog, StoreTransactionLogAdmin) diff --git a/kalite/store/hbtemplates/store/available-store-item.handlebars b/kalite/store/hbtemplates/store/available-store-item.handlebars new file mode 100644 index 0000000000..edc7dcfb0d --- /dev/null +++ b/kalite/store/hbtemplates/store/available-store-item.handlebars @@ -0,0 +1,8 @@ +
+ + +
Purchase ({{ cost }} points)
+
\ No newline at end of file diff --git a/kalite/store/hbtemplates/store/purchased-store-item.handlebars b/kalite/store/hbtemplates/store/purchased-store-item.handlebars new file mode 100644 index 0000000000..edc7dcfb0d --- /dev/null +++ b/kalite/store/hbtemplates/store/purchased-store-item.handlebars @@ -0,0 +1,8 @@ +
+ + +
Purchase ({{ cost }} points)
+
\ No newline at end of file diff --git a/kalite/store/hbtemplates/store/store-wrapper.handlebars b/kalite/store/hbtemplates/store/store-wrapper.handlebars new file mode 100644 index 0000000000..bcdfb86dc3 --- /dev/null +++ b/kalite/store/hbtemplates/store/store-wrapper.handlebars @@ -0,0 +1,4 @@ +

Items Available for Purchase

+
+

Your Purchased Items

+
\ No newline at end of file diff --git a/kalite/store/hbtemplates/store/store.handlebars b/kalite/store/hbtemplates/store/store.handlebars deleted file mode 100644 index dbb914d0f2..0000000000 --- a/kalite/store/hbtemplates/store/store.handlebars +++ /dev/null @@ -1 +0,0 @@ -Hear ye, hear ye! Spend your points here! \ No newline at end of file diff --git a/kalite/store/migrations/0002_auto__add_field_storetransactionlog_purchased_at.py b/kalite/store/migrations/0002_auto__add_field_storetransactionlog_purchased_at.py new file mode 100644 index 0000000000..5dea01703c --- /dev/null +++ b/kalite/store/migrations/0002_auto__add_field_storetransactionlog_purchased_at.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'StoreTransactionLog.purchased_at' + db.add_column(u'store_storetransactionlog', 'purchased_at', + self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'StoreTransactionLog.purchased_at' + db.delete_column(u'store_storetransactionlog', 'purchased_at') + + + models = { + 'securesync.device': { + 'Meta': {'object_name': 'Device'}, + 'counter': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.CharField', [], {'max_length': '32', 'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'public_key': ('django.db.models.fields.CharField', [], {'max_length': '500', 'db_index': 'True'}), + 'signature': ('django.db.models.fields.CharField', [], {'max_length': '360', 'null': 'True', 'blank': 'True'}), + 'signed_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['securesync.Device']"}), + 'signed_version': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'version': ('django.db.models.fields.CharField', [], {'default': "'0.9.2'", 'max_length': '9', 'blank': 'True'}), + 'zone_fallback': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['securesync.Zone']"}) + }, + 'securesync.facility': { + 'Meta': {'object_name': 'Facility'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '400', 'blank': 'True'}), + 'address_normalized': ('django.db.models.fields.CharField', [], {'max_length': '400', 'blank': 'True'}), + 'contact_email': ('django.db.models.fields.EmailField', [], {'max_length': '60', 'blank': 'True'}), + 'contact_name': ('django.db.models.fields.CharField', [], {'max_length': '60', 'blank': 'True'}), + 'contact_phone': ('django.db.models.fields.CharField', [], {'max_length': '60', 'blank': 'True'}), + 'counter': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.CharField', [], {'max_length': '32', 'primary_key': 'True'}), + 'latitude': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), + 'longitude': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'signature': ('django.db.models.fields.CharField', [], {'max_length': '360', 'null': 'True', 'blank': 'True'}), + 'signed_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['securesync.Device']"}), + 'signed_version': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'user_count': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'zone_fallback': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['securesync.Zone']"}), + 'zoom': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}) + }, + 'securesync.facilitygroup': { + 'Meta': {'object_name': 'FacilityGroup'}, + 'counter': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'facility': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['securesync.Facility']"}), + 'id': ('django.db.models.fields.CharField', [], {'max_length': '32', 'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '30'}), + 'signature': ('django.db.models.fields.CharField', [], {'max_length': '360', 'null': 'True', 'blank': 'True'}), + 'signed_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['securesync.Device']"}), + 'signed_version': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'zone_fallback': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['securesync.Zone']"}) + }, + 'securesync.facilityuser': { + 'Meta': {'object_name': 'FacilityUser'}, + 'counter': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'default_language': ('django.db.models.fields.CharField', [], {'max_length': '8', 'null': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'facility': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['securesync.Facility']"}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['securesync.FacilityGroup']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.CharField', [], {'max_length': '32', 'primary_key': 'True'}), + 'is_teacher': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '60', 'blank': 'True'}), + 'notes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'signature': ('django.db.models.fields.CharField', [], {'max_length': '360', 'null': 'True', 'blank': 'True'}), + 'signed_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['securesync.Device']"}), + 'signed_version': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '30'}), + 'zone_fallback': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['securesync.Zone']"}) + }, + 'securesync.zone': { + 'Meta': {'object_name': 'Zone'}, + 'counter': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.CharField', [], {'max_length': '32', 'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'signature': ('django.db.models.fields.CharField', [], {'max_length': '360', 'null': 'True', 'blank': 'True'}), + 'signed_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['securesync.Device']"}), + 'signed_version': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'zone_fallback': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['securesync.Zone']"}) + }, + u'store.storeitem': { + 'Meta': {'object_name': 'StoreItem'}, + 'cost': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'counter': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.CharField', [], {'max_length': '32', 'primary_key': 'True'}), + 'resource_id': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'resource_type': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'returnable': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'signature': ('django.db.models.fields.CharField', [], {'max_length': '360', 'null': 'True', 'blank': 'True'}), + 'signed_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['securesync.Device']"}), + 'signed_version': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'thumbnail': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'zone_fallback': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['securesync.Zone']"}) + }, + u'store.storetransactionlog': { + 'Meta': {'object_name': 'StoreTransactionLog'}, + 'context_id': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'context_type': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'counter': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.CharField', [], {'max_length': '32', 'primary_key': 'True'}), + 'item': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['store.StoreItem']"}), + 'purchased_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'reversible': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'signature': ('django.db.models.fields.CharField', [], {'max_length': '360', 'null': 'True', 'blank': 'True'}), + 'signed_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['securesync.Device']"}), + 'signed_version': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['securesync.FacilityUser']"}), + 'value': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'zone_fallback': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['securesync.Zone']"}) + } + } + + complete_apps = ['store'] \ No newline at end of file diff --git a/kalite/store/models.py b/kalite/store/models.py index d4a61af044..bbe3e92ebb 100644 --- a/kalite/store/models.py +++ b/kalite/store/models.py @@ -52,6 +52,7 @@ class StoreTransactionLog(DeferredCountSyncedModel): # can this transaction be undone by user actions? Initially set by 'returnable' on StoreItem, but can be changed subsequently reversible = models.BooleanField(default=False) item = models.ForeignKey(StoreItem, db_index=True) + purchased_at = models.DateTimeField(blank=True, null=True) class Meta: # needed to clear out the app_name property from SyncedClass.Meta pass diff --git a/kalite/store/static/css/store/store.css b/kalite/store/static/css/store/store.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/kalite/store/static/js/store/models.js b/kalite/store/static/js/store/models.js index dd8221ec20..de44c781bc 100644 --- a/kalite/store/static/js/store/models.js +++ b/kalite/store/static/js/store/models.js @@ -2,7 +2,30 @@ window.StoreItemModel = Backbone.Model.extend({ }); -window.StoreItemCollection = Backbone.Collection.extend({ + +window.AvailableStoreItemCollection = Backbone.Collection.extend({ + + url: "/api/store/storeitem/", model: StoreItemModel }); + + +window.PurchasedStoreItemCollection = Backbone.Collection.extend({ + + model: StoreItemModel, + + url: function() { + return "/api/store/storetransactionlog/?" + $.param({ + user: statusModel.get("user_id") + }); + } + +}); + + +window.StoreStateModel = Backbone.Collection.extend({ + defaults: { + points_remaining: 0 + } +}) \ No newline at end of file diff --git a/kalite/store/static/js/store/views.js b/kalite/store/static/js/store/views.js index 2fdf912c7b..b6bd64e8f3 100644 --- a/kalite/store/static/js/store/views.js +++ b/kalite/store/static/js/store/views.js @@ -1,6 +1,6 @@ -window.StoreView = Backbone.View.extend({ +window.StoreWrapperView = Backbone.View.extend({ - template: HB.template("store/store"), + template: HB.template("store/store-wrapper"), events: { @@ -10,7 +10,13 @@ window.StoreView = Backbone.View.extend({ _.bindAll(this); - // this.listenTo(this.model, "change:active", this.render); + this.render(); + + this.purchased_items = new PurchasedStoreItemCollection; + this.available_items = new AvailableStoreItemCollection; + + this.purchased_items.fetch(); + this.available_items.fetch(); }, @@ -21,3 +27,60 @@ window.StoreView = Backbone.View.extend({ }); + +window.AvailableStoreItemListView = Backbone.View.extend({ + + item_template: HB.template("store/available-store-item"), + + initialize: function() { + this.listenTo(this.collection, "add", this.add_item); + this.listenTo(this.collection, "reset", this.add_all_items); + }, + + add_item: function(item) { + console.log(item); + }, + + add_all_items: function() { + this.$el.html(""); + + } + +}); + + +window.PurchasedStoreItemListView = Backbone.View.extend({ + + item_template: HB.template("store/store-item"), + + initialize: function() { + this.listenTo(this.collection, "add", this.add_item); + this.listenTo(this.collection, "reset", this.add_all_items); + }, + + add_item: function(item) { + console.log(item); + }, + + add_all_items: function() { + this.$el.html(""); + + } + +}); + + +window.AvailableStoreItemView = Backbone.View.extend({ + + item_template: HB.template("store/available-store-item") + +}); + +window.PurchasedStoreItemView = Backbone.View.extend({ + + item_template: HB.template("store/purchased-store-item") + + + +}); + diff --git a/kalite/store/templates/store/store.html b/kalite/store/templates/store/store.html index 338f1bb6d6..a5f637822c 100644 --- a/kalite/store/templates/store/store.html +++ b/kalite/store/templates/store/store.html @@ -16,10 +16,11 @@ From 93ff7f58f833bcd30534ea9788807758114a0a46 Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Tue, 12 Aug 2014 11:39:28 -0700 Subject: [PATCH 4/9] Rendering available store items, remaining points, and basic styling. --- .../js/distributed/distributed-server.js | 14 ++-- .../store/available-store-item.handlebars | 3 +- .../store/store-wrapper.handlebars | 11 ++- kalite/store/static/css/store/store.css | 24 ++++++ kalite/store/static/js/store/views.js | 76 +++++++++++++++---- kalite/store/templates/store/store.html | 1 + kalite/store/views.py | 3 + 7 files changed, 105 insertions(+), 27 deletions(-) diff --git a/kalite/distributed/static/js/distributed/distributed-server.js b/kalite/distributed/static/js/distributed/distributed-server.js index 95a55cd936..270576c225 100644 --- a/kalite/distributed/static/js/distributed/distributed-server.js +++ b/kalite/distributed/static/js/distributed/distributed-server.js @@ -70,6 +70,9 @@ var StatusModel = Backbone.Model.extend({ this.loaded.then(this.after_loading); + this.listenTo(this, "change:points", this.update_total_points); + this.listenTo(this, "change:newpoints", this.update_total_points); + }, get_server_time: function () { @@ -98,8 +101,9 @@ var StatusModel = Backbone.Model.extend({ }); }, - get_current_points: function() { - return this.get("points") + this.get("newpoints"); + update_total_points: function() { + // add the points that existed at page load and the points earned since page load, to get the total current points + this.set("totalpoints", this.get("points") + this.get("newpoints")); } }); @@ -115,16 +119,14 @@ var TotalPointView = Backbone.View.extend({ initialize: function() { _.bindAll(this); - this.model.bind("change:points", this.render); - this.model.bind("change:newpoints", this.render); + this.model.bind("change:totalpoints", this.render); this.model.bind("change:username", this.render); this.render(); }, render: function() { - // add the points that existed at page load and the points earned since page load, to get the total current points - var points = this.model.get("points") + this.model.get("newpoints"); + var points = this.model.get("totalpoints"); var username_span = sprintf("%s", this.model.get("username")); var message = null; diff --git a/kalite/store/hbtemplates/store/available-store-item.handlebars b/kalite/store/hbtemplates/store/available-store-item.handlebars index edc7dcfb0d..aaf56e0e79 100644 --- a/kalite/store/hbtemplates/store/available-store-item.handlebars +++ b/kalite/store/hbtemplates/store/available-store-item.handlebars @@ -3,6 +3,7 @@ -
Purchase ({{ cost }} points)
+
\ No newline at end of file diff --git a/kalite/store/hbtemplates/store/store-wrapper.handlebars b/kalite/store/hbtemplates/store/store-wrapper.handlebars index bcdfb86dc3..0a12b88767 100644 --- a/kalite/store/hbtemplates/store/store-wrapper.handlebars +++ b/kalite/store/hbtemplates/store/store-wrapper.handlebars @@ -1,4 +1,7 @@ -

Items Available for Purchase

-
-

Your Purchased Items

-
\ No newline at end of file +

Items Available for Purchase ({{ points }} points remaining)

+
+
+

+

Your Purchased Items

+
+
\ No newline at end of file diff --git a/kalite/store/static/css/store/store.css b/kalite/store/static/css/store/store.css index e69de29bb2..eab75d500e 100644 --- a/kalite/store/static/css/store/store.css +++ b/kalite/store/static/css/store/store.css @@ -0,0 +1,24 @@ +.store-item { + background-color: #EEE; + border-radius: 10px; + padding: 10px; + margin-bottom: 10px; +} + +.store-item-image { + width: 20%; + max-width: 200px; + float: left; +} + +.store-item-metadata { + width: 75%; + float: left; + padding-left: 3%; +} + +.store-item-title { + font-weight: bold; + font-size: 1.2em; +} + diff --git a/kalite/store/static/js/store/views.js b/kalite/store/static/js/store/views.js index b6bd64e8f3..eb8097f394 100644 --- a/kalite/store/static/js/store/views.js +++ b/kalite/store/static/js/store/views.js @@ -12,38 +12,65 @@ window.StoreWrapperView = Backbone.View.extend({ this.render(); - this.purchased_items = new PurchasedStoreItemCollection; this.available_items = new AvailableStoreItemCollection; + this.purchased_items = new PurchasedStoreItemCollection; + + this.available_item_view = new AvailableStoreItemListView({ + collection: this.available_items, + el: this.$(".available-store-items") + }); + + this.purchased_item_view = new PurchasedStoreItemListView({ + collection: this.purchased_items, + el: this.$(".purchased-store-items") + }); - this.purchased_items.fetch(); this.available_items.fetch(); + this.purchased_items.fetch(); + + this.listenTo(statusModel, "change:totalpoints", this.update_points); }, render: function() { - this.$el.html(this.template()); // this.model.attributes + this.$el.html(this.template({ + points: statusModel.get("totalpoints") + })); return this; }, + update_points: function() { + this.$(".points-remaining").text(statusModel.get("totalpoints")); + } + }); window.AvailableStoreItemListView = Backbone.View.extend({ - item_template: HB.template("store/available-store-item"), - initialize: function() { + + _.bindAll(this); + + this.item_views = []; + this.listenTo(this.collection, "add", this.add_item); this.listenTo(this.collection, "reset", this.add_all_items); }, add_item: function(item) { - console.log(item); + var view = new AvailableStoreItemView({ + model: item + }); + this.$el.append(view.render().el); + this.item_views.push(view); }, add_all_items: function() { - this.$el.html(""); - + _.each(this.item_views, function(view) { + view.remove(); + }); + this.collection.each(this.add_item); } }); @@ -51,20 +78,29 @@ window.AvailableStoreItemListView = Backbone.View.extend({ window.PurchasedStoreItemListView = Backbone.View.extend({ - item_template: HB.template("store/store-item"), - initialize: function() { + + _.bindAll(this); + + this.item_views = []; + this.listenTo(this.collection, "add", this.add_item); this.listenTo(this.collection, "reset", this.add_all_items); }, add_item: function(item) { - console.log(item); + var view = new PurchasedStoreItemView({ + model: item + }); + this.$el.append(view.render().el); + this.item_views.push(view); }, add_all_items: function() { - this.$el.html(""); - + _.each(this.item_views, function(view) { + view.remove(); + }); + this.collection.each(this.add_item); } }); @@ -72,15 +108,23 @@ window.PurchasedStoreItemListView = Backbone.View.extend({ window.AvailableStoreItemView = Backbone.View.extend({ - item_template: HB.template("store/available-store-item") + template: HB.template("store/available-store-item"), + + render: function() { + this.$el.html(this.template(this.model.attributes)); + return this; + } }); window.PurchasedStoreItemView = Backbone.View.extend({ - item_template: HB.template("store/purchased-store-item") - + template: HB.template("store/purchased-store-item"), + render: function() { + this.$el.html(this.template(this.model.attributes)); + return this; + } }); diff --git a/kalite/store/templates/store/store.html b/kalite/store/templates/store/store.html index a5f637822c..a362808f21 100644 --- a/kalite/store/templates/store/store.html +++ b/kalite/store/templates/store/store.html @@ -4,6 +4,7 @@ {% load kalite_staticfiles %} {% block headcss %}{{ block.super }} + {% endblock headcss %} {% block headjs %}{{ block.super }} diff --git a/kalite/store/views.py b/kalite/store/views.py index c8a8a7d1fe..a812f6a662 100644 --- a/kalite/store/views.py +++ b/kalite/store/views.py @@ -10,6 +10,9 @@ from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext as _ +from kalite.shared.decorators import require_login + +@require_login @render_to("store/store.html") def store(request): return {} From 438cb592ce697f2a3f66a655c810fc4005d27dcf Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Tue, 12 Aug 2014 18:13:32 -0700 Subject: [PATCH 5/9] Basic purchasing from store is functional, with point deduction. --- .../js/distributed/distributed-server.js | 3 ++ kalite/store/admin.py | 2 +- kalite/store/api_resources.py | 2 + .../store/purchased-store-item.handlebars | 2 +- kalite/store/static/css/store/store.css | 3 ++ kalite/store/static/js/store/models.js | 16 +++--- kalite/store/static/js/store/views.js | 50 +++++++++++++++++-- 7 files changed, 63 insertions(+), 15 deletions(-) diff --git a/kalite/distributed/static/js/distributed/distributed-server.js b/kalite/distributed/static/js/distributed/distributed-server.js index 270576c225..368a38362a 100644 --- a/kalite/distributed/static/js/distributed/distributed-server.js +++ b/kalite/distributed/static/js/distributed/distributed-server.js @@ -99,6 +99,9 @@ var StatusModel = Backbone.Model.extend({ toggle_state("student", !self.get("is_admin") && !self.get("is_django_user") && self.get("is_logged_in")); toggle_state("admin", self.get("is_admin")); // combination of teachers & super-users }); + + this.update_total_points(); + }, update_total_points: function() { diff --git a/kalite/store/admin.py b/kalite/store/admin.py index 32710a4709..29a8db29ed 100644 --- a/kalite/store/admin.py +++ b/kalite/store/admin.py @@ -9,5 +9,5 @@ class StoreItemAdmin(admin.ModelAdmin): class StoreTransactionLogAdmin(admin.ModelAdmin): - list_display = ("item", "user", "purchased_at") + list_display = ("id", "item", "user", "purchased_at") admin.site.register(StoreTransactionLog, StoreTransactionLogAdmin) diff --git a/kalite/store/api_resources.py b/kalite/store/api_resources.py index fbabf910e5..bd5f433488 100644 --- a/kalite/store/api_resources.py +++ b/kalite/store/api_resources.py @@ -19,9 +19,11 @@ class Meta: "resource_type": ('exact', ), } + class StoreTransactionLogResource(ModelResource): user = fields.ForeignKey(FacilityUserResource, 'user') + item = fields.ForeignKey(StoreItemResource, 'item') class Meta: queryset = StoreTransactionLog.objects.all() diff --git a/kalite/store/hbtemplates/store/purchased-store-item.handlebars b/kalite/store/hbtemplates/store/purchased-store-item.handlebars index edc7dcfb0d..93030406dc 100644 --- a/kalite/store/hbtemplates/store/purchased-store-item.handlebars +++ b/kalite/store/hbtemplates/store/purchased-store-item.handlebars @@ -4,5 +4,5 @@
{{ title }}
{{ description }}
-
Purchase ({{ cost }} points)
+
\ No newline at end of file diff --git a/kalite/store/static/css/store/store.css b/kalite/store/static/css/store/store.css index eab75d500e..84e9bb92a3 100644 --- a/kalite/store/static/css/store/store.css +++ b/kalite/store/static/css/store/store.css @@ -22,3 +22,6 @@ font-size: 1.2em; } +.store-item-purchase-button { + padding: 5px 10px; +} \ No newline at end of file diff --git a/kalite/store/static/js/store/models.js b/kalite/store/static/js/store/models.js index de44c781bc..12c5e0cf4c 100644 --- a/kalite/store/static/js/store/models.js +++ b/kalite/store/static/js/store/models.js @@ -1,19 +1,22 @@ -window.StoreItemModel = Backbone.Model.extend({ +window.AvailableStoreItemModel = Backbone.Model.extend({ }); +window.PurchasedStoreItemModel = Backbone.Model.extend({ + urlRoot: "/api/store/storetransactionlog/" +}); window.AvailableStoreItemCollection = Backbone.Collection.extend({ url: "/api/store/storeitem/", - model: StoreItemModel + model: AvailableStoreItemModel }); window.PurchasedStoreItemCollection = Backbone.Collection.extend({ - model: StoreItemModel, + model: PurchasedStoreItemModel, url: function() { return "/api/store/storetransactionlog/?" + $.param({ @@ -22,10 +25,3 @@ window.PurchasedStoreItemCollection = Backbone.Collection.extend({ } }); - - -window.StoreStateModel = Backbone.Collection.extend({ - defaults: { - points_remaining: 0 - } -}) \ No newline at end of file diff --git a/kalite/store/static/js/store/views.js b/kalite/store/static/js/store/views.js index eb8097f394..c1e658f6a6 100644 --- a/kalite/store/static/js/store/views.js +++ b/kalite/store/static/js/store/views.js @@ -22,7 +22,8 @@ window.StoreWrapperView = Backbone.View.extend({ this.purchased_item_view = new PurchasedStoreItemListView({ collection: this.purchased_items, - el: this.$(".purchased-store-items") + el: this.$(".purchased-store-items"), + available_items: this.available_items }); this.available_items.fetch(); @@ -30,6 +31,8 @@ window.StoreWrapperView = Backbone.View.extend({ this.listenTo(statusModel, "change:totalpoints", this.update_points); + this.listenTo(this.available_item_view, "purchase_requested", this.make_purchase); + }, render: function() { @@ -41,6 +44,34 @@ window.StoreWrapperView = Backbone.View.extend({ update_points: function() { this.$(".points-remaining").text(statusModel.get("totalpoints")); + }, + + make_purchase: function(item) { + var points_remaining = statusModel.get("totalpoints"); + var cost = item.get("cost"); + + if (cost > points_remaining) { + alert("Sorry, you don't have enough points to purchase that right now."); + return; + } + + var purchased_model = new PurchasedStoreItemModel({ + item: item.id, + purchased_at: statusModel.get_server_time(), + reversible: item.get("returnable"), + context_id: 0, // TODO-BLOCKER: put the current unit in here + context_type: "unit", + user: statusModel.get("user_uri"), + value: -cost + }); + + purchased_model.save(); + + // add the item to the collection of purchased items so it will show in that list + this.purchased_items.add(purchased_model); + + // decrement the visible number of remaining points + statusModel.set("newpoints", statusModel.get("newpoints") - cost); } }); @@ -59,11 +90,13 @@ window.AvailableStoreItemListView = Backbone.View.extend({ }, add_item: function(item) { + var self = this; var view = new AvailableStoreItemView({ model: item }); this.$el.append(view.render().el); this.item_views.push(view); + this.listenTo(view, "purchase_requested", function(item) { self.trigger("purchase_requested", item); }) }, add_all_items: function() { @@ -90,7 +123,8 @@ window.PurchasedStoreItemListView = Backbone.View.extend({ add_item: function(item) { var view = new PurchasedStoreItemView({ - model: item + model: item, + available_items: this.options.available_items }); this.$el.append(view.render().el); this.item_views.push(view); @@ -110,9 +144,17 @@ window.AvailableStoreItemView = Backbone.View.extend({ template: HB.template("store/available-store-item"), + events: { + "click .store-item-purchase-button": "purchase_button_clicked" + }, + render: function() { this.$el.html(this.template(this.model.attributes)); return this; + }, + + purchase_button_clicked: function() { + this.trigger("purchase_requested", this.model); } }); @@ -122,7 +164,9 @@ window.PurchasedStoreItemView = Backbone.View.extend({ template: HB.template("store/purchased-store-item"), render: function() { - this.$el.html(this.template(this.model.attributes)); + // retrieve the item object itself, for rendering + var item = this.options.available_items.get(this.model.get("item")); + this.$el.html(this.template(item.attributes)); return this; } From 7243b0aa0bbbcf854a2e0c3a916ccf0106c776e5 Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Tue, 12 Aug 2014 18:29:57 -0700 Subject: [PATCH 6/9] Display smaller icons for list of purchased items. --- .../store/purchased-store-item.handlebars | 7 ++----- kalite/store/static/css/store/store.css | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/kalite/store/hbtemplates/store/purchased-store-item.handlebars b/kalite/store/hbtemplates/store/purchased-store-item.handlebars index 93030406dc..93ff1d1ba6 100644 --- a/kalite/store/hbtemplates/store/purchased-store-item.handlebars +++ b/kalite/store/hbtemplates/store/purchased-store-item.handlebars @@ -1,8 +1,5 @@ -
+
- +
{{ title }}
\ No newline at end of file diff --git a/kalite/store/static/css/store/store.css b/kalite/store/static/css/store/store.css index 84e9bb92a3..29939b9a69 100644 --- a/kalite/store/static/css/store/store.css +++ b/kalite/store/static/css/store/store.css @@ -1,16 +1,27 @@ -.store-item { +.store-item, .store-purchased-item { background-color: #EEE; border-radius: 10px; padding: 10px; margin-bottom: 10px; } -.store-item-image { +.store-item .store-item-image { width: 20%; max-width: 200px; float: left; } +.store-purchased-item { + float: left; + margin-right: 10px; +} + +.store-purchased-item .store-item-image { + height: 10%; + max-height: 100px; + float: left; +} + .store-item-metadata { width: 75%; float: left; From e30df505a54aba15f76e3bf90e1c73db55c85458 Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Tue, 12 Aug 2014 18:30:43 -0700 Subject: [PATCH 7/9] Gracefully handle 201 responses from tastypie (don't show error). --- static-libraries/js/khan-lite.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/static-libraries/js/khan-lite.js b/static-libraries/js/khan-lite.js index 3d20d2f475..410c72aeda 100644 --- a/static-libraries/js/khan-lite.js +++ b/static-libraries/js/khan-lite.js @@ -133,7 +133,12 @@ function handleFailedAPI(resp, error_prefix) { break; case 200: // return JSON messages + case 201: case 500: // also currently return JSON messages + + // handle empty responses gracefully + resp.responseText = resp.responseText || "{}"; + try { messages = $.parseJSON(resp.responseText); } catch (e) { From 6be6efa5d390428a40f46abf7375fd0a0cbe75a7 Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Wed, 13 Aug 2014 15:58:18 -0700 Subject: [PATCH 8/9] Comment out bootstrap-subset styles that interfere with top search bar. --- .../css/control_panel/bootstrap-control-panel.css | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/kalite/control_panel/static/css/control_panel/bootstrap-control-panel.css b/kalite/control_panel/static/css/control_panel/bootstrap-control-panel.css index 865d0cbd6c..ffa6c0f697 100644 --- a/kalite/control_panel/static/css/control_panel/bootstrap-control-panel.css +++ b/kalite/control_panel/static/css/control_panel/bootstrap-control-panel.css @@ -9,7 +9,7 @@ * Config saved to config.json and https://gist.github.com/90ea88eec00c6a7c0e94 */ /*! normalize.css v3.0.1 | MIT License | git.io/normalize */ -html { +/*html { font-family: sans-serif; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; @@ -263,7 +263,7 @@ th { font-family: 'Glyphicons Halflings'; src: url('../../fonts/control_panel/glyphicons-halflings-regular.eot'); src: url('../../fonts/control_panel/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../../fonts/control_panel/glyphicons-halflings-regular.woff') format('woff'), url('../../fonts/control_panel/glyphicons-halflings-regular.ttf') format('truetype'), url('../../fonts/control_panel/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); -} +}*/ .glyphicon { position: relative; top: 1px; @@ -875,7 +875,7 @@ th { .glyphicon-tree-deciduous:before { content: "\e200"; } -* { +/** { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; @@ -885,8 +885,8 @@ th { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; -} -html { +}*/ +/*html { font-size: 10px; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } @@ -924,7 +924,7 @@ figure { } img { vertical-align: middle; -} +}*/ .img-responsive { display: block; width: 100% \9; From aa23b437f93eff88776524cc6d3c302c9ebecb1c Mon Sep 17 00:00:00 2001 From: Jamie Alexandre Date: Wed, 13 Aug 2014 15:58:43 -0700 Subject: [PATCH 9/9] Style store buttons with bootstrap. --- .../store/hbtemplates/store/available-store-item.handlebars | 2 +- kalite/store/static/css/store/store.css | 4 ---- kalite/store/static/js/store/views.js | 6 +++++- kalite/store/templates/store/store.html | 1 + 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/kalite/store/hbtemplates/store/available-store-item.handlebars b/kalite/store/hbtemplates/store/available-store-item.handlebars index aaf56e0e79..e8df037f21 100644 --- a/kalite/store/hbtemplates/store/available-store-item.handlebars +++ b/kalite/store/hbtemplates/store/available-store-item.handlebars @@ -3,7 +3,7 @@
\ No newline at end of file diff --git a/kalite/store/static/css/store/store.css b/kalite/store/static/css/store/store.css index 29939b9a69..5f2f4911d2 100644 --- a/kalite/store/static/css/store/store.css +++ b/kalite/store/static/css/store/store.css @@ -32,7 +32,3 @@ font-weight: bold; font-size: 1.2em; } - -.store-item-purchase-button { - padding: 5px 10px; -} \ No newline at end of file diff --git a/kalite/store/static/js/store/views.js b/kalite/store/static/js/store/views.js index c1e658f6a6..b8d088b8b3 100644 --- a/kalite/store/static/js/store/views.js +++ b/kalite/store/static/js/store/views.js @@ -72,6 +72,7 @@ window.StoreWrapperView = Backbone.View.extend({ // decrement the visible number of remaining points statusModel.set("newpoints", statusModel.get("newpoints") - cost); + } }); @@ -153,7 +154,10 @@ window.AvailableStoreItemView = Backbone.View.extend({ return this; }, - purchase_button_clicked: function() { + purchase_button_clicked: function(ev) { + $(ev.target) + .switchClass("btn-primary", "btn-success", 100) + .switchClass("btn-success", "btn-primary", 400); this.trigger("purchase_requested", this.model); } diff --git a/kalite/store/templates/store/store.html b/kalite/store/templates/store/store.html index a362808f21..148342888e 100644 --- a/kalite/store/templates/store/store.html +++ b/kalite/store/templates/store/store.html @@ -5,6 +5,7 @@ {% block headcss %}{{ block.super }} + {% endblock headcss %} {% block headjs %}{{ block.super }}