Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRogelio Mita <rogeliomita@activitycentral.com>2013-03-30 00:34:47 (GMT)
committer Rogelio Mita <rogeliomita@activitycentral.com>2013-03-30 00:34:47 (GMT)
commitd7dd2038673159cc5f492140621fa18c43040775 (patch)
treebc10d191943053409586c77bfe8e132b0ce027e3
parent425150443c6de1377d952916b84f4edcd411cd67 (diff)
parent344e320ad6d6b2e4e84f0089546fb0b6befe51ca (diff)
Merge with DEVv2.0
-rw-r--r--.gitignore7
-rw-r--r--CeibalEncuesta/CeibalEncuesta.py479
-rw-r--r--CeibalEncuesta/CssStyle.css26
-rw-r--r--CeibalEncuesta/Globales.py150
-rw-r--r--CeibalEncuesta/Iconos/screen.svg146
-rw-r--r--CeibalEncuesta/Widgets.py947
-rw-r--r--webapp/README3
-rw-r--r--webapp/accounts/__init__.py (copied from webapp/webapp/media/output/empty)0
-rw-r--r--webapp/accounts/templates/login.html50
-rw-r--r--webapp/accounts/urls.py14
-rw-r--r--webapp/custom_admin/__init__.py (copied from webapp/webapp/media/output/empty)0
-rw-r--r--webapp/custom_admin/admin.py16
-rw-r--r--webapp/custom_admin/fixtures/initial_data.json20
-rw-r--r--webapp/custom_admin/models.py (copied from webapp/webapp/media/output/empty)0
-rw-r--r--webapp/custom_admin/urls.py10
-rw-r--r--webapp/db/empty (copied from webapp/webapp/media/output/empty)0
-rw-r--r--webapp/deploy/dev_testing/nginx.conf24
-rw-r--r--webapp/deploy/dev_testing/settings.py25
-rw-r--r--webapp/deploy/dev_testing/uwsgi.ini11
-rw-r--r--webapp/deploy/fabfile.py50
-rw-r--r--webapp/deploy/staging/nginx.conf4
-rw-r--r--webapp/deploy/staging/settings.py21
-rw-r--r--webapp/deploy/testing/nginx.conf24
-rw-r--r--webapp/deploy/testing/settings.py25
-rw-r--r--webapp/deploy/testing/uwsgi.ini11
-rw-r--r--webapp/js_tests/files.json11
-rw-r--r--webapp/js_tests/fixtures/container.html12
-rw-r--r--webapp/js_tests/spec/DynamicStructureSpec.js204
-rw-r--r--webapp/polls/exceptions.py4
-rw-r--r--webapp/polls/forms.py58
-rw-r--r--webapp/polls/management/__init__.py (copied from webapp/webapp/media/output/empty)0
-rw-r--r--webapp/polls/management/commands/__init__.py (copied from webapp/webapp/media/output/empty)0
-rw-r--r--webapp/polls/management/commands/syncindex.py18
-rw-r--r--webapp/polls/models.py633
-rw-r--r--webapp/polls/templates/base-poll.html11
-rw-r--r--webapp/polls/templates/builder.html153
-rw-r--r--webapp/polls/templates/image_option_thumbnail.html7
-rw-r--r--webapp/polls/templates/mustache/field.html56
-rw-r--r--webapp/polls/templates/mustache/group.html37
-rw-r--r--webapp/polls/templates/mustache/option.html34
-rw-r--r--webapp/polls/templates/mustache/option_default.html13
-rw-r--r--webapp/polls/templates/mustache/option_image_thumbnail.html24
-rw-r--r--webapp/polls/templates/mustache/option_image_upload.html31
-rw-r--r--webapp/polls/templates/poll-form.html81
-rw-r--r--webapp/polls/templates/poll-list.html50
-rw-r--r--webapp/polls/templates/poll-structure-form.html62
-rw-r--r--webapp/polls/templates/poll-success.html19
-rw-r--r--webapp/polls/templates/sucess.html28
-rw-r--r--webapp/polls/templates/tags/field-options.html243
-rw-r--r--webapp/polls/templates/tags/field-widget-types.html6
-rw-r--r--webapp/polls/templates/tags/structure.html71
-rw-r--r--webapp/polls/templatetags/poll_tags.py48
-rw-r--r--webapp/polls/tests.py214
-rw-r--r--webapp/polls/tests/__init__.py (copied from webapp/webapp/media/output/empty)0
-rw-r--r--webapp/polls/tests/field_tests.py211
-rw-r--r--webapp/polls/tests/group_tests.py79
-rw-r--r--webapp/polls/tests/option_tests.py155
-rw-r--r--webapp/polls/tests/poll_tests.py136
-rw-r--r--webapp/polls/tests/structure_tests.py219
-rw-r--r--webapp/polls/urls.py17
-rw-r--r--webapp/polls/views.py288
-rw-r--r--webapp/requirements6
-rw-r--r--webapp/utils/data_structure.py7
-rw-r--r--webapp/utils/decorators.py98
-rw-r--r--webapp/utils/forms.py2
-rw-r--r--webapp/utils/mongo_connection.py169
-rw-r--r--webapp/utils/test.py56
-rw-r--r--webapp/webapp/env_settings.py.sample21
-rw-r--r--webapp/webapp/media/image_options/empty (renamed from webapp/webapp/media/output/empty)0
-rw-r--r--webapp/webapp/settings.py44
-rwxr-xr-xwebapp/webapp/static/css/bootstrap-fileupload.css132
-rw-r--r--webapp/webapp/static/css/custom_admin.css35
-rw-r--r--webapp/webapp/static/img/no_image.gifbin0 -> 553 bytes
-rw-r--r--webapp/webapp/static/jasmine-jquery-latest.js288
-rw-r--r--webapp/webapp/static/jasmine-latest/MIT.LICENSE20
-rw-r--r--webapp/webapp/static/jasmine-latest/jasmine-html.js190
-rw-r--r--webapp/webapp/static/jasmine-latest/jasmine.css166
-rw-r--r--webapp/webapp/static/jasmine-latest/jasmine.js2476
-rw-r--r--webapp/webapp/static/jasmine-latest/jasmine_favicon.pngbin0 -> 905 bytes
-rwxr-xr-xwebapp/webapp/static/js/bootstrap-fileupload.js169
-rw-r--r--webapp/webapp/static/js/dynamic_structure.js479
-rw-r--r--webapp/webapp/templates/admin/base_site.html19
-rw-r--r--webapp/webapp/templates/base-main-public.html18
-rw-r--r--webapp/webapp/templates/base-main.html55
-rw-r--r--webapp/webapp/urls.py19
85 files changed, 8365 insertions, 1400 deletions
diff --git a/.gitignore b/.gitignore
index d97ae94..b35f4ab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,8 @@
*.pyc
*.pyo
*.bak
-webapp/webapp/media/output/*.json
-webapp/webapp/env_settings.py \ No newline at end of file
+webapp/webapp/media/cache/
+webapp/webapp/media/image_options/*
+!webapp/webapp/media/image_options/empty
+webapp/webapp/env_settings.py
+database.db \ No newline at end of file
diff --git a/CeibalEncuesta/CeibalEncuesta.py b/CeibalEncuesta/CeibalEncuesta.py
index 1e637a4..8d0e856 100644
--- a/CeibalEncuesta/CeibalEncuesta.py
+++ b/CeibalEncuesta/CeibalEncuesta.py
@@ -18,18 +18,50 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+# Notas:
+# Las listas de Encuestados se abren desde un archivo csv.
+# Las encuestas sin Responder se abren desde un archivo slv (shelve) o json.
+# Las Encuestas respondidas o parcialmente respondidas se guardan en archivos .encuesta (json)
+
import os
import sys
import gi
from gi.repository import Gtk
+from gi.repository import Gdk
+from gi.repository import GObject
from Widgets import Panel
from Widgets import My_FileChooser
+from Widgets import My_Alert_Dialog
import Globales as G
PATH = os.path.dirname(__file__)
+HOME = os.environ["HOME"]
+WORKPATH = os.path.join(HOME, "CeibalEncuesta")
+TEMPPATH = os.path.join(WORKPATH, 'temp.encuesta')
+
+if not os.path.exists(WORKPATH):
+ os.mkdir(WORKPATH)
+ os.chmod(WORKPATH, 0755)
+
+archivo = open(TEMPPATH, 'w')
+archivo.close()
+
+screen = Gdk.Screen.get_default()
+css_provider = Gtk.CssProvider()
+style_path = os.path.join(PATH, "CssStyle.css")
+css_provider.load_from_path(style_path)
+context = Gtk.StyleContext()
+
+context.add_provider_for_screen(
+ screen,
+ css_provider,
+ Gtk.STYLE_PROVIDER_PRIORITY_USER)
+
+GObject.threads_init()
+Gdk.threads_init()
class CeibalEncuesta(Gtk.Window):
@@ -45,69 +77,179 @@ class CeibalEncuesta(Gtk.Window):
self.set_resizable(True)
self.set_size_request(640, 480)
- self.set_border_width(2)
+ self.set_border_width(5)
self.set_position(Gtk.WindowPosition.CENTER)
+ self.path = TEMPPATH
+
+ self.out_dict = {} # keys = Encuestados, Values = Encuesta respondida
+
box = Gtk.VBox()
self.panel = Panel()
- box.pack_start(self.get_menu(), False, False, 0)
+ # menuitems
+ self.guardar = None
+ self.exportar = None
+
+ box.pack_start(self.__get_menu(), False, False, 0)
box.pack_start(self.panel, True, True, 0)
self.add(box)
self.show_all()
+ self.panel.hide()
+
self.panel.connect("new", self.__change)
+ self.panel.connect("text", self.__set_text)
self.panel.connect("new-selection", self.__new_selection)
+
self.connect("destroy", self.__salir)
- self.out_dict = None
+ def do_draw(self, context):
+ """
+ Pinta una imagen si no se ha cargado una lista
+ a encuestar o una encuesta a aplicar.
+ """
+
+ if self.panel.get_visible(): return False
+
+ from gi.repository import GdkPixbuf
+ from gi.repository import Gdk
+
+ import cairo
+
+ archivo = os.path.join(PATH,
+ "Iconos", "screen.svg")
- def __change(self, widget, new_dict):
- """Recibe los cambios en la encuesta."""
+ pixbuf = GdkPixbuf.Pixbuf.new_from_file(archivo)
- if self.out_dict == None:
- self.out_dict = {}
+ rect = self.get_allocation()
+ x, y, w, h = (rect.x, rect.y, rect.width, rect.height)
+ ww, hh = pixbuf.get_width(), pixbuf.get_height()
+
+ scaledPixbuf = pixbuf.scale_simple(
+ w, h, GdkPixbuf.InterpType.BILINEAR)
+
+ surface = cairo.ImageSurface(
+ cairo.FORMAT_ARGB32,
+ scaledPixbuf.get_width(),
+ scaledPixbuf.get_height())
+
+ tmpcontext = cairo.Context(surface)
+ Gdk.cairo_set_source_pixbuf(tmpcontext, scaledPixbuf, 0, 0)
+ tmpcontext.paint()
+ context.set_source_surface(surface)
+ context.paint()
+
+ def __set_text(self, widget,
+ encuestado, id_grupo, id_pregunta, id_opcion, text):
+ """
+ Cuando se ingresa texto en una opción
+ del tipo Entry. (caso particular).
+ """
+
+ user = ''
+ for item in encuestado:
+ user += ' %s' % item
+
+ encuestado = user.strip()
+
+ if text:
+ self.out_dict[encuestado][id_grupo]['fields'][id_pregunta]['options'][id_opcion]['newtext'] = text
+
+ else:
+ del self.out_dict[encuestado][id_grupo]['fields'][id_pregunta]['options'][id_opcion]['newtext']
+
+ self.__save_json(path = self.path)
+
+ def __change(self, widget, encuestado,
+ indice_grupo, grupo_name,
+ indice_pregunta, dict_pregunta):
+ """
+ Recibe los cambios en la encuesta y
+ almacena los datos.
+ """
+
+ user = ''
+ for item in encuestado:
+ user += ' %s' % item
+
+ encuestado = user.strip()
+
+ # Entrada para encuestado
+ if not encuestado in self.out_dict.keys():
+ self.out_dict[encuestado] = {}
- self.out_dict[new_dict.keys()[0]] = new_dict[new_dict.keys()[0]]
+ # Entrada para grupo
+ if not indice_grupo in self.out_dict[encuestado].keys():
+ self.out_dict[encuestado][indice_grupo] = {}
+
+ # Entrada para pregunta
+ if not 'fields' in self.out_dict[encuestado][indice_grupo].keys():
+ self.out_dict[encuestado][indice_grupo]['fields'] = {}
+
+ self.out_dict[encuestado][indice_grupo]['name'] = grupo_name
+
+ self.out_dict[encuestado][indice_grupo]['fields'][indice_pregunta] = dict_pregunta
- #print self.out_dict
+ self.__save_json(path = self.path)
def __new_selection(self, widget, data):
- """Cuando el usuario cambia de Encuestado."""
+ """
+ Cuando el usuario cambia de Encuestado.
+ """
- if self.out_dict != None:
- if data in self.out_dict.keys():
- self.panel.update(self.out_dict[data])
+ if data in self.out_dict.keys():
+ self.panel.update(self.out_dict[data])
- def get_menu(self):
- """Crea y devuelve el menú de la aplicación."""
+ def __get_menu(self):
+ """
+ Crea y devuelve el menú de la aplicación.
+ """
menu_bar = Gtk.MenuBar()
menu_bar.show()
file_menu = Gtk.Menu()
- alumnos = Gtk.MenuItem("Cargar Encuestados")
- encuesta = Gtk.MenuItem("Cargar Encuesta")
- #guardar = Gtk.MenuItem("Guardar")
+ encuestados = Gtk.MenuItem("Nueva Lista . . .")
+ encuesta = Gtk.MenuItem("Nueva Encuesta . . .")
+ recuperar = Gtk.MenuItem("Abrir Encuesta . . .")
+ self.guardar = Gtk.MenuItem("Guardar Encuesta . . .")
+ self.exportar = Gtk.MenuItem("Exportar Encuesta . . .")
salir = Gtk.MenuItem("Salir")
- file_menu.append(alumnos)
+ encuestados.set_tooltip_text(
+ "Cargar una Nueva Lista a Encuestar.")
+ encuesta.set_tooltip_text(
+ "Cargar una Nueva Encuesta para ser Aplicada a la Lista.")
+ recuperar.set_tooltip_text(
+ "Abrir una Encuesta Total o Parcialmente Respondida.")
+ self.guardar.set_tooltip_text(
+ "Guardar la Encuesta Actual.")
+ self.exportar.set_tooltip_text(
+ "Exportar Encuesta a Formato csv")
+ salir.set_tooltip_text(
+ "Salir de la Aplicación.")
+
+ file_menu.append(encuestados)
file_menu.append(encuesta)
- #file_menu.append(guardar)
+ file_menu.append(recuperar)
+ file_menu.append(self.guardar)
+ #file_menu.append(self.exportar)
file_menu.append(salir)
- alumnos.connect_object ("activate", self.__cargar_alumnos, '')
- encuesta.connect_object ("activate", self.__cargar_encuesta, '')
- #guardar.connect_object ("activate", self.__guardar, '')
- salir.connect_object ("activate", self.__salir, '')
+ encuestados.connect_object("activate", self.__cargar_encuestados, '')
+ encuesta.connect_object("activate", self.__cargar_encuesta, '')
+ recuperar.connect_object("activate", self.__recuperar_encuesta, '')
+ self.guardar.connect_object("activate", self.__guardar_encuesta, '')
+ self.exportar.connect_object("activate", self.__exportar_encuesta, '')
+ salir.connect_object("activate", self.__salir, '')
- alumnos.show()
- #guardar.show()
+ encuestados.show()
salir.show()
file_item = Gtk.MenuItem("Archivo")
@@ -116,44 +258,233 @@ class CeibalEncuesta(Gtk.Window):
file_item.set_submenu(file_menu)
menu_bar.append(file_item)
+ self.guardar.set_sensitive(False)
+ self.exportar.set_sensitive(False)
+
return menu_bar
- def __cargar_alumnos(self, widget = None, senial = None):
- """Abre Filechooser para cargar el archivo con la
- lista de alumnos a encuestar."""
+ def __exportar_encuesta(self, widget = None, senial = None):
+ """
+ Abre Filechooser para Exportar la encuesta a un
+ archivo csv.
+ """
+
+ filechooser = My_FileChooser(
+ parent_window = self,
+ action_type = Gtk.FileChooserAction.SAVE,
+ filter_type = 'text/csv',
+ title = "Exportar Encuesta")
+
+ filechooser.connect('load', self.__export_encuesta)
+
+ def __export_encuesta(self, widget, archivo):
+ """
+ Exporta la encuesta a un archivo csv.
+ """
+
+ name, ext = os.path.splitext(archivo)
+
+ if ext:
+ if ext != "csv":
+ ext = ".csv"
+
+ else:
+ ext = ".csv"
+
+ archivo = "%s%s" % (name, ext)
+
+ print "Exportar:", archivo
+
+ def __guardar_encuesta(self, widget = None, senial = None):
+ """
+ Abre Filechooser para guardar la
+ encuesta respondida o parcialmente respondida.
+ """
+
+ filechooser = My_FileChooser(
+ parent_window = self,
+ action_type = Gtk.FileChooserAction.SAVE,
+ filter_type = 'text/encuesta',
+ title = "Guardar Encuesta")
+
+ filechooser.connect('load', self.__save_encuesta)
+
+ def __save_encuesta(self, widget, archivo):
+ """
+ Guarda encuesta en formato json.
+ """
+
+ name, ext = os.path.splitext(archivo)
+
+ if ext:
+ if ext != "encuesta":
+ ext = ".encuesta"
+
+ else:
+ ext = ".encuesta"
+
+ archivo = "%s%s" % (name, ext)
+
+ self.__save_json(path = os.path.join(archivo))
+
+ # FIXME: A partir de este momento, la encuesta se
+ # guardará automáticamente en este archivo, analizar
+ # mejor si esto debe hacerse así o solo debe guardarse
+ # acá cuando el usuario lo decide.
+
+ def __recuperar_encuesta(self, widget = None, senial = None):
+ """
+ Abre Filechooser para cargar el archivo con la
+ encuesta respondida o parcialmente respondida.
+ """
+
+ if self.panel.lista and self.panel.encuesta:
+ dialog = My_Alert_Dialog(parent_window = self,
+ label = "¿ Proceder sin Guardar ?")
+
+ response = dialog.run()
+
+ dialog.destroy()
+
+ if Gtk.ResponseType(response) == Gtk.ResponseType.CANCEL:
+ return
+
+ filechooser = My_FileChooser(
+ parent_window = self,
+ action_type = Gtk.FileChooserAction.OPEN,
+ #filter_type = 'text/encuesta',
+ title = "Recuperar Encuesta")
+
+ filechooser.connect('load', self.__load_encuesta_respondida)
+
+ def __load_encuesta_respondida(self, widget, archivo):
+ """
+ Carga una encuesta parcial o totalmente
+ respondida, desde un archivo json.
+ """
+
+ if os.path.exists(TEMPPATH):
+ os.remove(TEMPPATH)
+
+ self.path = archivo
+ extension = os.path.splitext(os.path.split(archivo)[1])[1]
+
+ if 'encuesta' in extension:
+ import json
+ import codecs
+
+ archivo = codecs.open(archivo, "r", "utf-8")
+ enc = json.JSONDecoder("utf-8").decode(archivo.read())
+
+ self.out_dict = enc['groups']
+ self.panel.load_encuesta(enc['encuesta'])
+ self.panel.load_encuestados(enc['encuestados'])
+
+ def __cargar_encuestados(self, widget = None, senial = None):
+ """
+ Abre Filechooser para cargar el archivo con la
+ lista a encuestar.
+ """
+
+ if self.panel.lista and self.panel.encuesta:
+ dialog = My_Alert_Dialog(parent_window = self,
+ label = "¿ Proceder sin Guardar ?")
+
+ response = dialog.run()
+
+ dialog.destroy()
+
+ if Gtk.ResponseType(response) == Gtk.ResponseType.CANCEL:
+ return
+
filechooser = My_FileChooser(
parent_window = self,
action_type = Gtk.FileChooserAction.OPEN,
- filter_type = 'text/csv')
+ filter_type = 'text/csv',
+ title = "Cargar Lista a Encuestar")
- filechooser.connect('load', self.__load_alumnos)
-
- def __load_alumnos(self, widget, archivo):
- """Recibe archivo csv con la lista de alumnos a encuestar y
- la manda cargar en la aplicación."""
+ filechooser.connect('load', self.__load_encuestados)
+
+ def __load_encuestados(self, widget, archivo):
+ """
+ Recibe archivo csv con la lista a encuestar
+ y la manda cargar en la aplicación.
+ """
- self.out_dict = None
+ if os.path.exists(TEMPPATH):
+ os.remove(TEMPPATH)
- alumnos = G.cargar_alumnos(os.path.join(archivo))
- self.panel.load_alumnos(alumnos)
+ self.path = TEMPPATH
+
+ arch = open(self.path, 'w')
+ arch.close()
+
+ self.out_dict = {}
+ encuestados = G.cargar_encuestados(os.path.join(archivo))
+
+ for encue in encuestados[1:]:
+ user = ''
+
+ for item in encue:
+ user += ' %s' % item
+
+ encuestado = user.strip()
+
+ if not encuestado in self.out_dict.keys():
+ self.out_dict[encuestado] = {}
+
+ self.panel.load_encuestados(encuestados)
+
+ GObject.idle_add(self.__save_json, self.path)
+
+ self.__check_sensitive()
def __cargar_encuesta(self, widget = None, senial = None):
- """Abre Filechooser para cargar el
- archivo con la encuesta."""
+ """
+ Abre Filechooser para cargar el
+ archivo con la encuesta.
+ """
+
+ if self.panel.lista and self.panel.encuesta:
+ dialog = My_Alert_Dialog(parent_window = self,
+ label = "¿ Proceder sin Guardar ?")
+
+ response = dialog.run()
+
+ dialog.destroy()
+
+ if Gtk.ResponseType(response) == Gtk.ResponseType.CANCEL:
+ return
filechooser = My_FileChooser(
parent_window = self,
action_type = Gtk.FileChooserAction.OPEN,
- filter_type = None)
+ filter_type = None,
+ title = "Cargar Nueva Encuesta")
filechooser.connect('load', self.__load_encuesta)
-
+
def __load_encuesta(self, widget, archivo):
- """Carga una encuesta almacenada en
- un archivo json o en un archivo shelve."""
+ """
+ Carga una encuesta almacenada en
+ un archivo json o shelve.
+ """
- self.out_dict = None
+ if os.path.exists(TEMPPATH):
+ os.remove(TEMPPATH)
+
+ self.path = TEMPPATH
+
+ arch = open(self.path, 'w')
+ arch.close()
+
+ # Mantiene la lista de encuestados cargada actualmente.
+ encuestados = self.out_dict.keys()
+
+ for encuestado in encuestados:
+ self.out_dict[encuestado] = {}
+
encuesta = {}
extension = os.path.splitext(os.path.split(archivo)[1])[1]
@@ -163,35 +494,63 @@ class CeibalEncuesta(Gtk.Window):
import codecs
archivo = codecs.open(archivo, "r", "utf-8")
- encuesta = json.JSONDecoder("utf-8").decode(archivo.read())
+ enc = json.JSONDecoder("utf-8").decode(archivo.read())
+ encuesta = enc['groups']
elif 'slv' in extension:
-
import shelve
archivo = shelve.open(archivo)
- for key in archivo.keys():
- encuesta[key] = archivo[key]
+ enc = archivo['groups']
+
+ for key in enc.keys():
+ encuesta[key] = enc[key]
archivo.close()
-
+
self.panel.load_encuesta(encuesta)
- def __guardar(self, widget = None, senial = None):
- """Abre Filechooser para guardar la encuesta."""
+ GObject.idle_add(self.__save_json, self.path)
- filechooser = My_FileChooser(
- parent_window = self,
- action_type = Gtk.FileChooserAction.OPEN,
- filter_type = 'text/csv')
-
- #filechooser.connect('load', self.__load_alumnos)
-
+ self.__check_sensitive()
+
+ self.panel.update({})
+
+ def __check_sensitive(self):
+
+ if self.panel.encuesta and self.panel.lista:
+ self.guardar.set_sensitive(True)
+ self.exportar.set_sensitive(True)
+
+ else:
+ self.guardar.set_sensitive(False)
+ self.exportar.set_sensitive(False)
+
+ def __save_json(self, path = TEMPPATH):
+ """
+ Guarda encuesta en formato json.
+ """
+
+ self.path = path
+
+ salida = {
+ 'encuestados': self.panel.encuestados,
+ 'groups': self.out_dict,
+ 'encuesta': self.panel.encuesta}
+
+ import simplejson
+ archivo = open(self.path, 'w')
+ archivo.write(simplejson.dumps(salida))
+ archivo.close()
+
+ if os.path.exists(TEMPPATH):
+ if TEMPPATH != path:
+ os.remove(TEMPPATH)
+
def __salir(self, widget = None, senial = None):
sys.exit(0)
-
if __name__ == "__main__":
diff --git a/CeibalEncuesta/CssStyle.css b/CeibalEncuesta/CssStyle.css
new file mode 100644
index 0000000..c4c02f1
--- /dev/null
+++ b/CeibalEncuesta/CssStyle.css
@@ -0,0 +1,26 @@
+/* Contenedores */
+GtkWindow {
+ background-color: #d8eeb1;
+ color: #000000;
+}
+
+/* Toolbars */
+GtkToolbar {
+ background-color: #d8eeb1;
+ color: #000000;
+}
+
+GtkMenu {
+ background-color: #ffffff;
+ color: #000000;
+}
+
+.tooltip {
+ color: #ffffff;
+ background-color: #000000;
+ background-image: none;
+ border-color: #000000;
+ border-radius: 5px;
+ border-style: solid;
+ border-width: 1px;
+} \ No newline at end of file
diff --git a/CeibalEncuesta/Globales.py b/CeibalEncuesta/Globales.py
index 26658c7..28ba72e 100644
--- a/CeibalEncuesta/Globales.py
+++ b/CeibalEncuesta/Globales.py
@@ -19,130 +19,50 @@
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import csv
+import chardet
-def cargar_alumnos(csvfile):
+def cargar_encuestados(csvfile):
alumnos = []
-
- reader = csv.reader(open(csvfile, 'rb'), dialect='excel', delimiter=';')
+
+ archivo = open(csvfile, 'rb')
+ encoding = chardet.detect(archivo.read())['encoding']
+ archivo.seek(0)
+ reader = csv.reader(archivo, dialect='excel', delimiter=';')
for index, row in enumerate(reader):
+ row = [x.decode(encoding) for x in row]
alumnos.append(row)
return alumnos
-# EJEMPLOS:
-
-#Respuesta en texto: TextInput
-#Respuesta con lista de opciones: DropDownList
-#Respuesta con checks (multiple selección): MultipleCheckBox
-#Respuesta con radios (Solo una selección): RadioButton
-
-Encuesta = {
- '0': {
- 'name': 'Asistencia',
- 'fields': {
- '0': {
- 'widget_type': 'RadioButton',
- 'name': '¿El Alumno se Encuentra en Clase?',
- 'options':{
- '00001': {'text': 'Si'},
- '00002': {'text': 'No'},
- },
- 'dependence': ''
- }
- },
- },
- '1': {
- 'name': 'Datos de XO',
- 'fields': {
- '0': {
- 'widget_type': 'DropDownList',
- 'name': '¿Recibiste en Algún Momento tu XO?',
- 'options': {
- '00003': {'text': 'Si'},
- '00004': {'text': 'No'}
- },
- 'dependence': '00001'
- },
- '1': {
- 'widget_type': 'DropDownList',
- 'name': '¿Hoy Trajiste a Clase tu XO?',
- 'options': {
- '00005': {'text': 'Si'},
- '00006': {'text': 'No'}
- },
- 'dependence': '00001'
- },
- '2': {
- 'widget_type': 'DropDownList',
- 'name': '¿Cúal es el Estado Actual de tu XO?',
- 'options': {
- '00007': {'text': 'Totalmente Sana'},
- '00008': {'text': 'Parcialmente Sana'},
- '00009': {'text': 'No Funciona'}
- },
- 'dependence': '00001'
- }
- },
- },
- '2': {
- 'name': 'Uso de la XO',
- 'fields': {
- '0': {
- 'widget_type': 'MultipleCheckBox',
- 'name': '¿Para qué Utilizas tu XO?',
- 'options':{
- '00010': {'text': 'Facebook'},
- '00011': {'text': 'Juegos'},
- '00012': {'text': 'Estudiar'},
- '00013': {'text': 'Leer'},
- '00014': {'text': 'Escuchar Música'},
- '00015': {'text': 'Ver Videos'},
- '00016': {'text': 'Para Nada'}
- },
- 'dependence': '00001'
- }
- },
- },
- '3': {
- 'name': 'Comentarios',
- 'fields': {
- '0': {
- 'widget_type': 'TextInput',
- 'name': 'Comentarios',
- 'options': {
- '00017': {'text': ''}
- },
- 'dependence': ''
- }
- },
- }
-}
-
# EJEMPLO de Encuesta Contestada:
"""
-{'1101236 MONTEVIDEO 25 2 A 1 43210002 IGNACIO PEREZ ': {
- '0': {
- 'fields': {
- '0': {
- 'name': '\xc2\xbfEl Alumno se Encuentra en Clase?',
- 'options': {
- '00001': {'Si': False},
- '00002': {'No': True}}}},
- 'name': 'Asistencia'},
-
- '2': {
- 'fields': {
- '0': {
- 'name': '\xc2\xbfPara qu\xc3\xa9 Utilizas tu XO?',
- 'options': {
- '00005': {'Escuchar M\xc3\xbasica': False},
- '00004': {'Leer': False},
- '00007': {'Para Nada': False},
- '00006': {'Ver Videos': False},
- '00001': {'Facebook': True},
- '00003': {'Estudiar': False},
- '00002': {'Juegos': False}}}},
- 'name': 'Uso de la XO'}}}
+{
+"1101236 MONTEVIDEO 236 2 A 1 46415902 WILLIAM OPORTO": {},
+"1101236 MONTEVIDEO 236 2 A 1 46415902 IGNACIO PEREZ": {
+ "1": {
+ "fields": {
+ "0": {
+ "widget_type": "DropDownList",
+ "default": ["00003"],
+ "options": {
+ "00004": {"text": "No"},
+ "00003": {"text": "Si"}},
+ "dependence": "",
+ "name": "\u00bfRecibiste en Alg\u00fan Momento tu XO?"}},
+ "name": "Uso de la XO"}}, . . .
+
+En caso de una pregunta cuyo widget es un entry,
+se agrega clave newtext:
+
+ "fields": {
+ "0": {
+ "widget_type": "TextInput",
+ "default": ["00017"],
+ "options": {
+ "00017": {"newtext": "hola", "text": ""}},
+ "name": "Comentarios",
+ "dependence": ""}},
+ "name": "Comentarios"}}
""" \ No newline at end of file
diff --git a/CeibalEncuesta/Iconos/screen.svg b/CeibalEncuesta/Iconos/screen.svg
new file mode 100644
index 0000000..9a6a97a
--- /dev/null
+++ b/CeibalEncuesta/Iconos/screen.svg
@@ -0,0 +1,146 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ width="640"
+ height="480"
+ id="svg2"
+ inkscape:version="0.48.3.1 r9886"
+ sodipodi:docname="screen.svg">
+ <metadata
+ id="metadata29">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1366"
+ inkscape:window-height="712"
+ id="namedview27"
+ showgrid="false"
+ inkscape:zoom="0.54800776"
+ inkscape:cx="57.825092"
+ inkscape:cy="350.71956"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg2" />
+ <defs
+ id="defs4" />
+ <rect
+ style="fill:#d8eeb1;fill-opacity:0"
+ id="rect2985"
+ width="640.91241"
+ height="480"
+ x="0.10595738"
+ y="0.29492262"
+ ry="0" />
+ <g
+ id="g3756"
+ transform="matrix(3.0756929,0,0,3.0756929,25.205437,23.508369)">
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path3066-72-8"
+ d="m 5.3972179,26.199757 c 2.2223425,5.051966 12.7749441,17.837 27.9669601,9.404816 C 43.306195,31.02232 44.0682,11.905603 37.076607,7.7613636 32.277124,8.5153225 32.621695,15.527463 23.835612,21.249852 17.318236,25.251025 8.8839177,22.261598 5.3972179,26.199757 z"
+ style="fill:#81c900;fill-opacity:1;stroke:#000000;stroke-width:0.06929866;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ sodipodi:nodetypes="sccccccccscccc"
+ inkscape:connector-curvature="0"
+ id="path3066-5-2-7"
+ d="m 16.087045,4.7309368 c -1.819391,0.010603 -3.484943,0.3855362 -4.861734,1.1910706 -2.1077907,5.7156256 5.784425,9.5734566 6.680824,22.6628266 0.398173,9.562147 -8.0394056,17.001403 -5.857902,23.204221 4.43789,0.399354 14.510366,-1.045682 21.125262,-7.936862 -2.827129,-0.676143 -5.373521,-2.15472 -7.027317,-4.623519 -0.237521,-0.304311 -0.464325,-0.655036 -0.680741,-0.959232 3.999793,-0.744506 10.158829,-2.066333 13.847486,-8.536849 0.08602,-0.929174 0.127558,-1.89763 0.129935,-2.901882 C 40.336076,15.29955 25.911751,4.6737273 16.087045,4.7309368 z M 39.312923,29.732593 c -0.577075,6.233303 -2.957888,10.805177 -6.139428,14.1196 6.955506,1.663497 15.656835,-1.538385 17.227211,-6.908209 -1.305739,-3.783336 -5.778051,-3.02929 -11.087783,-7.211391 z"
+ style="fill:#81c900;fill-opacity:1;stroke:#000000;stroke-width:0.086823;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3066-72-8-5"
+ d="m 32.332012,11.993374 c -1.67525,2.663174 -3.754099,6.16036 -8.543225,9.279523 -2.116904,1.299616 -4.427155,1.864051 -6.713307,2.165583 0.375578,1.550599 0.659712,3.266877 0.790437,5.175744 0.123255,2.95997 -0.609533,5.703926 -1.602531,8.283354 2.663795,1.189685 5.746102,1.819034 9.171244,1.407629 -0.0028,-0.0039 -0.008,-0.0069 -0.01082,-0.01081 1.249937,-0.23266 2.714166,-0.522072 4.244543,-1.006996 0.05711,-0.01808 0.11595,-0.03549 0.173247,-0.05414 0.0423,-0.01549 0.08754,-0.02748 0.129934,-0.04331 1.088343,-0.406433 2.203448,-0.925154 3.345826,-1.559219 0.344956,-0.158989 0.684085,-0.337817 1.006997,-0.530568 0.01497,-0.01 0.02837,-0.02248 0.04331,-0.03247 1.151775,-0.771543 2.262089,-1.730498 3.27003,-2.945192 0.046,-0.05864 0.09558,-0.113755 0.140764,-0.173247 0.06078,-0.07543 0.113265,-0.160884 0.173246,-0.238214 0.498272,-0.684328 0.946924,-1.422532 1.342662,-2.198067 0.07147,-0.854352 0.106109,-1.736191 0.108279,-2.652839 0.427637,-5.520658 -2.662018,-10.834077 -7.070626,-14.866733 z"
+ style="fill:#006900;fill-opacity:1;stroke:#000000;stroke-width:0.06929866;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ sodipodi:nodetypes="cccc"
+ inkscape:connector-curvature="0"
+ id="path3917-0-1"
+ d="m 25.656115,38.344237 c -5.855303,-9.370125 -1.158406,-15.462208 3.748139,-19.940578 2.41378,3.76265 3.040258,9.396741 9.095449,12.622254 -2.656377,3.396352 -5.029672,5.491257 -12.843588,7.318324 z"
+ style="fill:#ff0000;stroke:#000000;stroke-width:0.08696981;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ </g>
+ <text
+ xml:space="preserve"
+ style="font-size:57.0641861px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+ x="248.80858"
+ y="96.14257"
+ id="text3762"
+ sodipodi:linespacing="125%"
+ transform="scale(0.85584433,1.1684368)"><tspan
+ sodipodi:role="line"
+ id="tspan3764"
+ x="248.80858"
+ y="96.14257">Ceibal Encuesta</tspan></text>
+ <rect
+ style="fill:#81c900;fill-opacity:0.30416667"
+ id="rect3766"
+ width="386.45163"
+ height="7.7419353"
+ x="215.37318"
+ y="124.32718" />
+ <text
+ xml:space="preserve"
+ style="font-size:20.88242149px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+ x="476.44223"
+ y="483.25589"
+ id="text3768"
+ sodipodi:linespacing="125%"
+ transform="scale(1.0444824,0.95741202)"><tspan
+ sodipodi:role="line"
+ id="tspan3770"
+ x="476.44223"
+ y="483.25589">Plan Ceibal</tspan></text>
+ <g
+ id="g3756-7"
+ transform="matrix(5.4307347,0,0,5.4307347,183.2027,159.07704)"
+ style="fill:#81c900;fill-opacity:0.30416667">
+ <path
+ sodipodi:nodetypes="ccccc"
+ inkscape:connector-curvature="0"
+ id="path3066-72-8-9"
+ d="m 5.3972179,26.199757 c 2.2223425,5.051966 12.7749441,17.837 27.9669601,9.404816 C 43.306195,31.02232 44.0682,11.905603 37.076607,7.7613636 32.277124,8.5153225 32.621695,15.527463 23.835612,21.249852 17.318236,25.251025 8.8839177,22.261598 5.3972179,26.199757 z"
+ style="fill:#81c900;fill-opacity:0.30416667" />
+ <path
+ sodipodi:nodetypes="sccccccccscccc"
+ inkscape:connector-curvature="0"
+ id="path3066-5-2-7-0"
+ d="m 16.087045,4.7309368 c -1.819391,0.010603 -3.484943,0.3855362 -4.861734,1.1910706 -2.1077907,5.7156256 5.784425,9.5734566 6.680824,22.6628266 0.398173,9.562147 -8.0394056,17.001403 -5.857902,23.204221 4.43789,0.399354 14.510366,-1.045682 21.125262,-7.936862 -2.827129,-0.676143 -5.373521,-2.15472 -7.027317,-4.623519 -0.237521,-0.304311 -0.464325,-0.655036 -0.680741,-0.959232 3.999793,-0.744506 10.158829,-2.066333 13.847486,-8.536849 0.08602,-0.929174 0.127558,-1.89763 0.129935,-2.901882 C 40.336076,15.29955 25.911751,4.6737273 16.087045,4.7309368 z M 39.312923,29.732593 c -0.577075,6.233303 -2.957888,10.805177 -6.139428,14.1196 6.955506,1.663497 15.656835,-1.538385 17.227211,-6.908209 -1.305739,-3.783336 -5.778051,-3.02929 -11.087783,-7.211391 z"
+ style="fill:#81c900;fill-opacity:0.30416667" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path3066-72-8-5-4"
+ d="m 32.332012,11.993374 c -1.67525,2.663174 -3.754099,6.16036 -8.543225,9.279523 -2.116904,1.299616 -4.427155,1.864051 -6.713307,2.165583 0.375578,1.550599 0.659712,3.266877 0.790437,5.175744 0.123255,2.95997 -0.609533,5.703926 -1.602531,8.283354 2.663795,1.189685 5.746102,1.819034 9.171244,1.407629 -0.0028,-0.0039 -0.008,-0.0069 -0.01082,-0.01081 1.249937,-0.23266 2.714166,-0.522072 4.244543,-1.006996 0.05711,-0.01808 0.11595,-0.03549 0.173247,-0.05414 0.0423,-0.01549 0.08754,-0.02748 0.129934,-0.04331 1.088343,-0.406433 2.203448,-0.925154 3.345826,-1.559219 0.344956,-0.158989 0.684085,-0.337817 1.006997,-0.530568 0.01497,-0.01 0.02837,-0.02248 0.04331,-0.03247 1.151775,-0.771543 2.262089,-1.730498 3.27003,-2.945192 0.046,-0.05864 0.09558,-0.113755 0.140764,-0.173247 0.06078,-0.07543 0.113265,-0.160884 0.173246,-0.238214 0.498272,-0.684328 0.946924,-1.422532 1.342662,-2.198067 0.07147,-0.854352 0.106109,-1.736191 0.108279,-2.652839 0.427637,-5.520658 -2.662018,-10.834077 -7.070626,-14.866733 z"
+ style="fill:#81c900;fill-opacity:0.30416667" />
+ <path
+ sodipodi:nodetypes="cccc"
+ inkscape:connector-curvature="0"
+ id="path3917-0-1-6"
+ d="m 25.656115,38.344237 c -5.855303,-9.370125 -1.158406,-15.462208 3.748139,-19.940578 2.41378,3.76265 3.040258,9.396741 9.095449,12.622254 -2.656377,3.396352 -5.029672,5.491257 -12.843588,7.318324 z"
+ style="fill:#81c900;fill-opacity:0.30416667" />
+ </g>
+</svg>
diff --git a/CeibalEncuesta/Widgets.py b/CeibalEncuesta/Widgets.py
index 43741a3..29099b5 100644
--- a/CeibalEncuesta/Widgets.py
+++ b/CeibalEncuesta/Widgets.py
@@ -19,35 +19,50 @@
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import os
+import base64
import gi
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GObject
from gi.repository import Pango
+from gi.repository import GdkPixbuf
+
+HOME = os.environ["HOME"]
+WORKPATH = os.path.join(HOME, "CeibalEncuesta")
class Panel(Gtk.Paned):
__gsignals__ = {
"new":(GObject.SIGNAL_RUN_FIRST,
- GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT, )),
+ GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT,
+ GObject.TYPE_PYOBJECT, GObject.TYPE_PYOBJECT,
+ GObject.TYPE_PYOBJECT, GObject.TYPE_PYOBJECT)),
"new-selection":(GObject.SIGNAL_RUN_FIRST,
- GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT, ))}
+ GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT,)),
+ "text":(GObject.SIGNAL_RUN_FIRST,
+ GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT,
+ GObject.TYPE_STRING, GObject.TYPE_STRING,
+ GObject.TYPE_STRING, GObject.TYPE_STRING))}
def __init__(self):
- Gtk.Paned.__init__(self, orientation = Gtk.Orientation.HORIZONTAL)
+ Gtk.Paned.__init__(self,
+ orientation = Gtk.Orientation.HORIZONTAL)
- self.lista = None # Lista() Lista de alumnos
+ self.encuestados = None # mantiene csv para guardarlo como tal
+ self.lista = None # Lista() Lista de encuestados
self.encuesta = None # Dict Encuesta
self.dependencies = None # Dependencias preguntas - respuestas
- self.out_dict = None # Persistencia
# Izquierda
box = Gtk.VBox()
self.scroll_list = Gtk.ScrolledWindow()
- self.scroll_list.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
+
+ self.scroll_list.set_policy(
+ Gtk.PolicyType.AUTOMATIC,
+ Gtk.PolicyType.AUTOMATIC)
# self.scroll_list.add_with_viewport(self.lista)
box.pack_start(self.scroll_list, True, True, 0)
@@ -55,42 +70,88 @@ class Panel(Gtk.Paned):
self.pack1(box, resize = False, shrink = True)
# Derecha
+ base_box = Gtk.VBox()
+
self.box_encuesta = Gtk.VBox()
scroll = Gtk.ScrolledWindow()
- scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
+
+ scroll.set_policy(
+ Gtk.PolicyType.AUTOMATIC,
+ Gtk.PolicyType.AUTOMATIC)
+
scroll.add_with_viewport(self.box_encuesta)
- self.pack2(scroll, resize = True, shrink = True)
+ toolbar_encuesta = ToolbarEncuesta()
+
+ base_box.pack_start(scroll, True, True, 0)
+ base_box.pack_start(toolbar_encuesta, False, False, 0)
+
+ self.pack2(base_box, resize = True, shrink = True)
self.show_all()
- def load_alumnos(self, alumnos):
- """Carga una lista de alumnos."""
+ self.scroll_list.hide()
+ self.box_encuesta.hide()
+
+ toolbar_encuesta.connect('accion', self.__accion_encuesta)
+
+ def __accion_encuesta(self, widget, accion):
+ """
+ Pasa la vista de un grupo de preguntas
+ a otro según valor de accion.
+ """
+
+ index_visible = 0
+
+ grupos = self.box_encuesta.get_children()
+
+ for child in grupos:
+ if child.get_visible():
+ index_visible = grupos.index(child)
+ break
+
+ if accion == 'Pag. Anterior':
+ if index_visible > 0:
+ map(self.__desactivar, grupos)
+ map(self.__activar, [grupos[index_visible - 1]])
+
+ elif accion == 'Pag. Siguiente':
+ if index_visible < len(grupos)-1:
+ map(self.__desactivar, grupos)
+ map(self.__activar, [grupos[index_visible + 1]])
+
+ def load_encuestados(self, encuestados):
+ """
+ Carga una lista a encuestar.
+ """
+
+ if not encuestados: return
- if not alumnos: return
+ self.show()
+ self.scroll_list.show_all()
- self.out_dict = None
if self.lista: self.lista.destroy()
- self.lista = Lista(alumnos)
- self.lista.connect('new-selection', self.new_selection)
+ self.encuestados = encuestados
+ self.lista = Lista(self.encuestados)
+ self.lista.connect('new-selection', self.__new_selection)
self.scroll_list.add_with_viewport(self.lista)
self.scroll_list.set_size_request(200,-1)
- if not self.encuesta:
- self.set_sensitive(False)
-
- else:
- self.set_sensitive(True)
+ self.__check_sensitive()
def load_encuesta(self, encuesta):
- """Carga una nueva Encuesta eliminando la anterior.
- Encuesta es un diccionario."""
+ """
+ Carga una nueva Encuesta eliminando la anterior.
+ Encuesta es un diccionario.
+ """
if not encuesta: return
- self.out_dict = None
+ self.show()
+ self.box_encuesta.show_all()
+
self.encuesta = encuesta
self.dependencies = {}
@@ -101,48 +162,97 @@ class Panel(Gtk.Paned):
# un grupo es un frame
grupos = self.encuesta.keys()
grupos.sort()
+
for indice in grupos:
# indice es el indice del grupo, Encuesta[indice] es el contenido
# cada grupo es un diccionario con dos keys (name y fields)
# name es el nombre del grupo == text del frame. fields son las preguntas
+
grupo = Grupo(indice, self.encuesta[indice])
self.box_encuesta.pack_start(grupo, False, True, 5)
- grupo.connect('new', self.change)
- grupo.connect('dependencies', self.set_dependencies)
- grupo.connect('check_dependence', self.check_dependence)
-
- if not self.lista:
- self.set_sensitive(False)
- else:
- self.set_sensitive(True)
-
- def change(self, widget, new_dict):
- """Recibe los cambios en grupo de preguntas y
- emite diccionario con encuesta respondida por
- el usuario activo."""
+ grupo.connect('new', self.__change)
+ grupo.connect('text', self.__emit_text)
+ grupo.connect('dependencies', self.__set_dependencies)
- modelo, iter = self.lista.treeselection.get_selected()
+ self.__check_sensitive()
- encuestado = ""
- for index in range(0, 8):
- encuestado += "%s " % modelo.get_value(iter, index)
+ grupos = self.box_encuesta.get_children()
+ map(self.__desactivar, grupos)
+ map(self.__activar, [grupos[0]])
- encuestado.strip()
+ def __emit_text(self, widget, id_pregunta, id_opcion, text):
+ """
+ Cuando se ingresa texto en una opción
+ del tipo Entry. (caso particular).
+ """
- if self.out_dict == None:
- self.out_dict = {}
-
- self.out_dict[widget.indice] = new_dict
+ encuestado = self.__get_encuestado()
+
+ self.emit(
+ 'text', encuestado,
+ widget.indice, id_pregunta,
+ id_opcion, text)
+
+ def __change(self, widget, pregunta, activan, desactivan):
+ """
+ Cuando se contesta una pregunta se checkean las
+ dependencias y se guardan los datos.
+ """
+
+ encuestado = self.__get_encuestado()
+
+ grupo_name = widget.grupo['name']
+ dict_pregunta = pregunta.pregunta.copy()
+ dict_pregunta['default'] = list(activan) # list(pregunta.widget_obtions.default)
+
+ indice_grupo = widget.indice
+ indice_pregunta = pregunta.indice
+
+ self.emit('new', encuestado, indice_grupo,
+ grupo_name, indice_pregunta, dict_pregunta)
+
+ activar = []
+ desactivar = []
+
+ for key in activan:
+ if key in self.dependencies.values():
+ for item in self.dependencies.items():
+ if item[1] == key:
+ activar.append(item[0])
+
+ for key in desactivan:
+ if key in self.dependencies.values():
+ for item in self.dependencies.items():
+ if item[1] == key:
+ desactivar.append(item[0])
+
+ map(self.__activar, activar)
+ map(self.__desactivar, desactivar)
+
+ def __get_encuestado(self):
+ """
+ Devuelve lista con datos del
+ encuestado seleccionado.
+ """
+
+ modelo, iter = self.lista.treeselection.get_selected()
- self.emit('new', {encuestado : self.out_dict})
+ encuestado = []
- def new_selection(self, widget, encuestado):
- """Cuando el usuario cambia de Encuestado."""
+ for index in range(0, 8):
+ encuestado.append(modelo.get_value(iter, index))
+
+ return encuestado
+
+ def __new_selection(self, widget, encuestado):
+ """
+ Cuando el usuario cambia de Encuestado.
+ """
self.emit('new-selection', encuestado)
- def set_dependencies(self, widget, dependencies):
+ def __set_dependencies(self, widget, dependencies):
"""
Al crearse un grupo, Recibe la lista de sus
preguntas que tienen dependencia a una respuesta
@@ -151,52 +261,69 @@ class Panel(Gtk.Paned):
for pregunta in dependencies:
self.dependencies[pregunta] = pregunta.pregunta['dependence']
- pregunta.set_sensitive(False)
-
- def check_dependence(self, widget, dict):
- """
- Cuando una pregunta es contestada, verifica las
- preguntas que dependen de su respuesta
- activándolas o desactivándolas según ésta.
- """
- for key in dict.keys():
-
- if key in self.dependencies.values():
-
- preguntas = []
- for item in self.dependencies.items():
- if item[1] == key:
- preguntas.append(item[0])
-
- if dict[key]:
- map(self.activar, preguntas)
-
- else:
- map(self.desactivar, preguntas)
-
- def activar(self, pregunta):
+ def __activar(self, widget):
+
+ widget.set_visible(True)
- pregunta.set_sensitive(True)
+ def __desactivar(self, widget):
- def desactivar(self, pregunta):
+ widget.set_visible(False)
- pregunta.set_sensitive(False)
+ def __check_sensitive(self):
+ if not self.encuesta:
+ self.box_encuesta.hide()
+
+ elif self.encuesta:
+ self.box_encuesta.show_all()
+
+ if not self.lista:
+ self.scroll_list.hide()
+
+ elif self.lista:
+ self.scroll_list.show_all()
+
+ if self.encuesta and self.lista:
+ self.set_sensitive(True)
+
+ else:
+ self.set_sensitive(False)
+
def update(self, dict):
- """Cuando se selecciona un usuario que ha
+ """
+ Cuando se selecciona un usuario que ha
contestado al menos parte de la encuesta, se
actualiza la interfaz gráfica de la misma con
- esos valores."""
-
- keys = dict.keys()
- keys.sort()
+ esos valores.
+ """
- grupos = self.box_encuesta.get_children()
+ self.load_encuesta(self.encuesta)
- for key in keys:
+ for child in self.box_encuesta.get_children():
+ if not child.indice in dict.keys(): continue
+ child.update(dict[child.indice])
- grupos[int(keys.index(key))].update(dict[key])
+ activadas = []
+
+ for key in dict.keys():
+ for pregunta in dict[key]['fields'].keys():
+ for default in dict[key]['fields'][pregunta]['default']:
+ activadas.append(default)
+
+ for child in self.box_encuesta.get_children():
+ for pregunta in child.box_preguntas.get_children():
+
+ if pregunta.pregunta.get('dependence', None):
+
+ if pregunta.pregunta['dependence'] in activadas:
+ pregunta.set_visible(True)
+
+ else:
+ pregunta.set_visible(False)
+
+ else:
+ pregunta.set_visible(True)
class My_FileChooser(Gtk.FileChooserDialog):
"""
@@ -206,20 +333,23 @@ class My_FileChooser(Gtk.FileChooserDialog):
__gsignals__ = {
'load':(GObject.SIGNAL_RUN_FIRST,
- GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT, ))}
+ GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT,))}
def __init__(self,
parent_window = None,
action_type = None,
- filter_type = None):
+ filter_type = None,
+ title = None):
Gtk.FileChooserDialog.__init__(self,
parent = parent_window,
- action = action_type)
+ action = action_type,
+ flags = Gtk.DialogFlags.MODAL,
+ title = title)
self.set_default_size( 640, 480 )
self.set_select_multiple(False)
- self.set_current_folder_uri("file:///%s" % os.path.dirname(__file__))
+ self.set_current_folder_uri("file:///%s" % WORKPATH)
if filter_type != None:
filter = Gtk.FileFilter()
@@ -229,7 +359,14 @@ class My_FileChooser(Gtk.FileChooserDialog):
hbox = Gtk.Box(orientation = Gtk.Orientation.HORIZONTAL)
- abrir = Gtk.Button("Abrir")
+ texto = ""
+ if action_type == Gtk.FileChooserAction.OPEN:
+ texto = "Abrir"
+
+ elif action_type == Gtk.FileChooserAction.SAVE:
+ texto = "Guardar"
+
+ abrir = Gtk.Button(texto)
salir = Gtk.Button("Salir")
hbox.pack_end(salir, True, True, 5)
@@ -252,16 +389,18 @@ class My_FileChooser(Gtk.FileChooserDialog):
self.destroy()
class Lista(Gtk.TreeView):
- """Lista Dinámica de Alumnos."""
+ """
+ Lista Dinámica de encuestados.
+ """
__gsignals__ = {
"new-selection":(GObject.SIGNAL_RUN_FIRST,
GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT, ))}
- def __init__(self, alumnos):
+ def __init__(self, encuestados):
- encabezado = alumnos[0]
- alumnos = alumnos[1:]
+ encabezado = encuestados[0]
+ encuestados = encuestados[1:]
Gtk.TreeView.__init__(self)
@@ -271,29 +410,25 @@ class Lista(Gtk.TreeView):
self.valor_select = None
- '''
- self.modelo = Gtk.ListStore(
- GObject.TYPE_STRING,
- GObject.TYPE_STRING,
- GObject.TYPE_STRING,
- GObject.TYPE_STRING,
- GObject.TYPE_STRING,
- GObject.TYPE_STRING,
- GObject.TYPE_STRING,
- GObject.TYPE_STRING)'''
-
strings = []
+
for x in encabezado:
strings.append(GObject.TYPE_STRING)
+
self.modelo = Gtk.ListStore(*strings)
for item in encabezado:
+ visible = False
+
+ if item == "CI" or item == "NOMBRE":
+ visible = True
+
self.append_column(
self.construir_columa(
item,
encabezado.index(item),
- True))
+ visible))
self.treeselection = self.get_selection()
@@ -303,19 +438,22 @@ class Lista(Gtk.TreeView):
self.set_model(self.modelo)
self.show_all()
- self.add_alumnos(alumnos)
+ self.add_encuestados(encuestados)
def selecciones(self, treeselection,
model, path, is_selected, listore):
- """Cuando se selecciona un item en la lista."""
+ """
+ Cuando se selecciona un item en la lista.
+ """
iter = model.get_iter(path)
encuestado = ""
+
for index in range(0, 8):
encuestado += "%s " % model.get_value(iter, index)
- encuestado.strip()
+ encuestado = encuestado.strip()
if not is_selected and self.valor_select != encuestado:
self.valor_select = encuestado
@@ -331,30 +469,37 @@ class Lista(Gtk.TreeView):
columna.set_property('visible', visible)
columna.set_property('resizable', False)
columna.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
+
return columna
- def add_alumnos(self, alumnos):
- """ Recibe lista de: alumnos y
- Comienza secuencia de agregado."""
+ def add_encuestados(self, encuestados):
+ """
+ Recibe lista de: encuestados y
+ Comienza secuencia de agregado.
+ """
- GObject.idle_add(self.run_add_alumno, alumnos)
+ GObject.idle_add(self.run_add_encuestado, encuestados)
- def run_add_alumno(self, alumnos):
- """Agrega los items a la lista,
- uno a uno, actualizando."""
+ def run_add_encuestado(self, encuestados):
+ """
+ Agrega los items a la lista,
+ uno a uno, actualizando.
+ """
- if not alumnos:
+ if not encuestados:
+
if not self.valor_select:
self.seleccionar_primero()
+
return
- alumno = alumnos[0]
+ encuestado = encuestados[0]
- self.modelo.append( alumno )
+ self.modelo.append( encuestado )
- alumnos.remove(alumno)
+ encuestados.remove(encuestado)
- GObject.idle_add(self.run_add_alumno, alumnos)
+ GObject.idle_add(self.run_add_encuestado, encuestados)
def seleccionar_primero(self, widget = None):
@@ -366,17 +511,20 @@ class Grupo(Gtk.Frame):
Un grupo es:
'0': {
- 'name': 'group 0',
- 'fields': { . . . => Preguntas
+ 'name': 'group 0',
+ 'fields': { . . . => Preguntas}
+ }
"""
__gsignals__ = {
"new":(GObject.SIGNAL_RUN_FIRST,
- GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT, )),
+ GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT,
+ GObject.TYPE_PYOBJECT, GObject.TYPE_PYOBJECT)),
"dependencies":(GObject.SIGNAL_RUN_FIRST,
- GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT, )),
- "check_dependence":(GObject.SIGNAL_RUN_FIRST,
- GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT, ))}
+ GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT,)),
+ "text":(GObject.SIGNAL_RUN_FIRST,
+ GObject.TYPE_NONE, (GObject.TYPE_STRING,
+ GObject.TYPE_STRING, GObject.TYPE_STRING))}
def __init__(self, indice_grupo, grupo):
@@ -384,7 +532,6 @@ class Grupo(Gtk.Frame):
self.indice = indice_grupo # indice del grupo en la encuesta
self.grupo = grupo # Diccionario del grupo en la encuesta
- self.out_dict = None # Persistencia de datos
self.set_label(self.grupo['name'])
@@ -393,6 +540,7 @@ class Grupo(Gtk.Frame):
preguntas = self.grupo['fields']
dependencies = []
+
keys = preguntas.keys()
keys.sort()
@@ -400,84 +548,81 @@ class Grupo(Gtk.Frame):
pregunta = Pregunta(indice, preguntas[indice])
self.box_preguntas.pack_start(pregunta, False, True, 3)
pregunta.connect('new', self.change)
- pregunta.connect('check_dependence', self.emit_check_dependence)
+ pregunta.connect('text', self.emit_text)
if pregunta.pregunta.get('dependence', ''):
- #if pregunta.pregunta['dependence']:
dependencies.append(pregunta)
self.show_all()
if dependencies:
GObject.idle_add(self.emit_dependencies, dependencies)
-
- def change(self, widget, new_dict):
- """Recibe los cambios en una pregunta y
- emite diccionario con el grupo de
- preguntas y sus respuestas."""
- if self.out_dict == None:
- self.out_dict = {
- 'name': self.grupo['name'],
- 'fields': {}
- }
+ def emit_text(self, widget, id_opcion, text):
+ """
+ Cuando se ingresa texto en una opción
+ del tipo Entry. (caso particular).
+ """
+
+ self.emit('text', widget.indice, id_opcion, text)
- self.out_dict['fields'][widget.indice] = new_dict
+ def change(self, widget, activan, desactivan):
+ """
+ Cuando se contesta una pregunta, checkea las
+ dependencias y se guardan los datos.
+ """
- self.emit('new', self.out_dict)
+ self.emit('new', widget, activan, desactivan)
def emit_dependencies(self, dependencies):
- """Luego de creado el grupo, se emite la lista
- de preguntas con dependencia a respuestas en
- otras preguntas."""
-
- self.emit('dependencies', dependencies)
-
- def emit_check_dependence(self, widget, dict):
"""
- Recibe 'check_dependence' de la pregunta que
- se ha respondido y:
- Emite 'check_dependence' con {opcion: valor, opcion:valor, . . .}
- para activar o desactivar otras preguntas.
+ Luego de creado el grupo, se emite la lista
+ de preguntas con dependencia a respuestas en
+ otras preguntas.
"""
- self.emit('check_dependence', dict)
+ self.emit('dependencies', dependencies)
def update(self, dict):
- """Cuando se selecciona un usuario que ha
+ """
+ Cuando se selecciona un usuario que ha
contestado al menos parte de la encuesta, se
actualiza la interfaz gráfica de la misma con
- esos valores."""
-
- keys = dict['fields'].keys()
- keys.sort()
-
- preguntas = self.box_preguntas.get_children()
+ esos valores.
+ """
- for key in keys:
+ for child in self.box_preguntas.get_children():
+
+ # Si se respondió esta pregunta
+ if child.indice in dict['fields'].keys():
+ child.update(dict['fields'][child.indice])
- preguntas[int(keys.index(key))].update(dict['fields'][key])
-
class Pregunta(Gtk.HBox):
"""
Box con Pregunta.
Una pregunta es:
+ {
'0': {
- 'widget_type': 'DropDownList',
- 'name': 'field 0 0',
- 'options': {
- '00001': {'text': 'opcion 1'},
- '00002': {'text': 'opcion 2'}
+ 'widget_type': 'RadioButton',
+ 'name': '¿El Alumno se Encuentra en Clase?',
+ 'options':{
+ '00001': {'text': 'Si'},
+ '00002': {'text': 'No'},
+ },
+ 'dependence': ''
}
+ }
"""
__gsignals__ = {
"new":(GObject.SIGNAL_RUN_FIRST,
- GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT, )),
- "check_dependence":(GObject.SIGNAL_RUN_FIRST,
- GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT, ))}
+ GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT,
+ GObject.TYPE_PYOBJECT)),
+ "text":(GObject.SIGNAL_RUN_FIRST,
+ GObject.TYPE_NONE, (GObject.TYPE_STRING,
+ GObject.TYPE_STRING))}
def __init__(self, indice_pregunta, pregunta):
@@ -494,58 +639,58 @@ class Pregunta(Gtk.HBox):
elif widget == 'TextInput':
self.widget_obtions = Widget_TextInput(self.pregunta['options'])
+ self.widget_obtions.connect('text', self.emit_text)
- elif widget == 'MultipleCheckBox':
+ elif widget == 'MultipleCheckBox' or widget == 'ImageCheckBox':
self.widget_obtions = Widget_MultipleCheckBox(self.pregunta['options'])
- elif widget == 'RadioButton':
+ elif widget == 'RadioButton' or widget == 'ImageRadioButton':
self.widget_obtions = Widget_RadioButon(self.pregunta['options'])
else:
print "Widget no Considerado", widget
label = Gtk.Label(self.pregunta['name'])
+
pangoFont = Pango.FontDescription("10")
+
label.modify_font(pangoFont)
label.modify_fg(0, Gdk.Color(0, 0, 65000))
self.pack_start(label, False, True, 5)
- self.pack_start(self.widget_obtions, False, True, 0)
+ self.pack_end(self.widget_obtions, True, True, 0)
self.show_all()
self.widget_obtions.connect('new', self.change)
- def change(self, widget, new_dict):
+ def emit_text(self, widget, id_opcion, text):
"""
- Recibe los cambios en las opciones de una pregunta y:
- Emite diccionario con las respuestas a las opciones.
- Emite 'check_dependence' con {opcion: valor, opcion:valor, . . .}
- para activar o desactivar otras preguntas.
+ Cuando se ingresa texto en una opción
+ del tipo Entry. (caso particular).
"""
-
- new_dict = {
- 'name': self.pregunta['name'],
- 'options': new_dict}
-
- self.emit('new', new_dict)
- check_dependence = {}
+ self.emit('text', id_opcion, text)
- for key in new_dict['options'].keys():
- for k in new_dict['options'][key].keys():
- check_dependence[key] = new_dict['options'][key][k]
-
- self.emit('check_dependence', check_dependence)
+ def change(self, widget, activan, desactivan):
+ """
+ Cuando se Contesta una pregunta,
+ se checkean las dependencia y se
+ guardan los datos.
+ """
+
+ self.emit('new', activan, desactivan)
def update(self, dict):
- """Cuando se selecciona un usuario que ha
+ """
+ Cuando se selecciona un usuario que ha
contestado al menos parte de la encuesta, se
actualiza la interfaz gráfica de la misma con
- esos valores."""
+ esos valores.
+ """
self.widget_obtions.update(dict)
-
+
class Widget_DropDownList(Gtk.ComboBoxText):
"""
Contenedor de opciones para respuestas posibles en una pregunta.
@@ -560,13 +705,15 @@ class Widget_DropDownList(Gtk.ComboBoxText):
__gsignals__ = {
"new":(GObject.SIGNAL_RUN_FIRST,
- GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT, ))}
+ GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT,
+ GObject.TYPE_PYOBJECT))}
def __init__(self, options):
Gtk.ComboBoxText.__init__(self)
self.options = options
+ self.updating = False
for key in self.options.keys():
self.append_text(self.options[key]['text'])
@@ -576,65 +723,57 @@ class Widget_DropDownList(Gtk.ComboBoxText):
def do_changed(self):
"""
Cuando el usuario hace click sobre una opción,
- se emite la señal new con los datos para todas las opciones.
-
- Traduce opciones de:
-
- 'options': {
- '00001': {'text': 'opcion 1'},
- '00002': {'text': 'opcion 2'}
- }
-
- a:
-
- 'options': {
- '00001': {Opcion: valor},
- '00002': {Opcion: valor}
- }
+ se checkean dependencias y se guardan los datos.
"""
- new_dict = {}
+ if self.updating: return
+
+ desactivan = []
+ activan = []
for key in self.options.keys():
-
if self.options[key]['text'] == self.get_active_text():
- new_dict[key] = {self.get_active_text(): True}
- else:
- new_dict[key] = {self.options[key]['text']: False}
+ if not key in activan:
+ activan.append(key)
+
+ elif not self.options[key]['text'] == self.get_active_text():
- self.emit('new', new_dict)
+ if key in activan:
+ activan.remove(key)
+
+ if not key in desactivan:
+ desactivan.append(key)
+
+ self.emit('new', activan, desactivan)
def update(self, dict):
- """Cuando se selecciona un usuario que ha
- contestado al menos parte de la encuesta, se
- actualiza la interfaz gráfica de la misma con
- esos valores."""
+ """
+ Cuando el usuario cambia de encuestado,
+ actualiza sus respuestas en la pregunta.
+ """
- pass
- #print "Widget_DropDownList", dict
- '''
- keys = dict['options'].keys()
- keys.sort()
-
- for key in keys:
- if dict['options'][key][dict['options'][key].keys()[0]]:
-
- model = self.get_model()
- item = model.get_iter_first()
- count = 0
-
- while item:
-
- if model.get_value(item, 0) == dict['options'][key].keys()[0]:
- self.set_active(count)
- item = None
-
- else:
- item = model.iter_next(item)
- count += 1
+ self.updating = True
+
+ model = self.get_model()
+ item = model.get_iter_first()
+
+ count = 0
- print "Widget_DropDownList", key'''
+ for default in dict['default']:
+
+ while item:
+ if model.get_value(item, 0) == \
+ dict['options'][default]['text']:
+
+ self.set_active(count)
+ self.updating = False
+ return
+
+ item = model.iter_next(item)
+ count += 1
+
+ self.updating = False
class Widget_TextInput(Gtk.Entry):
"""
@@ -649,7 +788,11 @@ class Widget_TextInput(Gtk.Entry):
__gsignals__ = {
"new":(GObject.SIGNAL_RUN_FIRST,
- GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT, ))}
+ GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT,
+ GObject.TYPE_PYOBJECT)),
+ "text":(GObject.SIGNAL_RUN_FIRST,
+ GObject.TYPE_NONE, (GObject.TYPE_STRING,
+ GObject.TYPE_STRING))}
def __init__(self, options):
@@ -657,39 +800,53 @@ class Widget_TextInput(Gtk.Entry):
self.options = options
+ self.updating = False
+
+ key = self.options.keys()[0]
+ self.set_text(self.options[key]['text'])
+
self.show_all()
def do_key_release_event(self, void):
"""
- Cuando el usuario suelta una tecla sobre el widget,
- se emite la señal new con los datos para la respuesta."""
-
+ Cuando el usuario ingresa algún valor en la opción,
+ se checkean dependencias.
"""
- Traduce opciones de:
- 'options':{
- '00001':{'text': ''}
- }
+ if self.updating: return
+
+ desactivan = []
+ activan = []
+
+ key = self.options.keys()[0]
+ text = self.get_text()
+
+ if text:
+ activan.append(key)
+
+ elif not text:
+ desactivan.append(key)
- a:
+ self.emit('new', activan, desactivan)
+ self.emit('text', key, text)
- 'options':{
- '00001':{Pregunta: Respuesta}
- }
+ def update(self, dict):
+ """
+ Cuando el usuario cambia de encuestado,
+ actualiza sus respuestas en la pregunta.
"""
- new_dict = {}
+ self.updating = True
- for key in self.options.keys():
- new_dict[key] = {
- self.get_parent().pregunta['name']: self.get_text()}
+ key = self.options.keys()[0]
+
+ if 'newtext' in self.options[key].keys():
+ self.set_text(self.options[key]['newtext'])
- self.emit('new', new_dict)
-
- def update(self, dict):
+ else:
+ self.set_text(self.options[key]['text'])
- pass
- #print "Widget_TextInput", dict
+ self.updating = False
class Widget_RadioButon(Gtk.ButtonBox):
"""
@@ -705,13 +862,15 @@ class Widget_RadioButon(Gtk.ButtonBox):
__gsignals__ = {
"new":(GObject.SIGNAL_RUN_FIRST,
- GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT, ))}
+ GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT,
+ GObject.TYPE_PYOBJECT))}
def __init__(self, options):
Gtk.ButtonBox.__init__(self)
self.options = options
+ self.updating = False
grupo = None
@@ -719,8 +878,24 @@ class Widget_RadioButon(Gtk.ButtonBox):
keys.sort()
for key in keys:
- radio = Gtk.RadioButton.new_with_label(
- None, self.options[key]['text'])
+
+ radio = MyRadioButton(key)
+
+ text = self.options[key].get('text', '')
+ imagen = self.options[key].get('img', '')
+
+ if text: radio.set_label(text)
+
+ if imagen:
+ loader = GdkPixbuf.PixbufLoader()
+ image_string = base64.b64decode(imagen)
+ loader.write(image_string)
+ loader.close()
+
+ pixbuf = loader.get_pixbuf().scale_simple(
+ 80, 80, GdkPixbuf.InterpType.BILINEAR)
+
+ radio.set_image(Gtk.Image.new_from_pixbuf(pixbuf))
if grupo == None: grupo = radio
@@ -736,44 +911,46 @@ class Widget_RadioButon(Gtk.ButtonBox):
def change(self, widget):
"""
Cuando el usuario hace click sobre una opción,
- se emite la señal new con los datos para todas las opciones.
+ se checkean dependencias y se guardan los datos.
+ """
- Traduce opciones de:
+ if self.updating: return
+
+ options = self.get_children()
- 'options': {
- '00001': {'text': 'opcion 1'},
- '00002': {'text': 'opcion 2'}
- }
+ desactivan = []
+ activan = []
+
+ for child in options:
- a:
+ if child.get_active():
+ activan.append(child.indice)
+
+ elif not child.get_active():
+ desactivan.append(child.indice)
+
+ self.emit('new', activan, desactivan)
- 'options': {
- '00001': {Opcion: valor},
- '00002': {Opcion: valor}
- }
+ def update(self, dict):
+ """
+ Cuando el usuario cambia de encuestado,
+ actualiza sus respuestas en la pregunta.
"""
- keys = self.options.keys()
- keys.sort()
-
- new_dict = {}
+ self.updating = True
options = self.get_children()
for child in options:
- indice = options.index(child)
-
- new_dict[keys[indice]] = {
- child.get_label(): child.get_active()}
-
- self.emit('new', new_dict)
-
- def update(self, dict):
+ if child.indice in dict['default']:
+ child.set_active(True)
+
+ else:
+ child.set_active(False)
+
+ self.updating = False
- pass
- # print "Widget_RadioButon", dict
-
-class Widget_MultipleCheckBox(Gtk.ButtonBox):
+class Widget_MultipleCheckBox(Gtk.Table):
"""
Contenedor de opciones para respuestas posibles en una pregunta.
@@ -787,63 +964,187 @@ class Widget_MultipleCheckBox(Gtk.ButtonBox):
__gsignals__ = {
"new":(GObject.SIGNAL_RUN_FIRST,
- GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT, ))}
+ GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT,
+ GObject.TYPE_PYOBJECT))}
def __init__(self, options):
- Gtk.ButtonBox.__init__(self)
+ columns = 2
+ rows = int(len(options)/columns)
+
+ while rows*columns < int(len(options)):
+ rows += 1
+
+ Gtk.Table.__init__(
+ self, rows = rows,
+ columns = columns,
+ homogeneous = True)
+
self.options = options
+ self.updating = False
keys = self.options.keys()
keys.sort()
+ col = 0
+ row = 0
+
for key in keys:
- check = Gtk.CheckButton.new_with_label(
- self.options[key]['text'])
+
+ check = MyCheckButton(key)
+
+ text = self.options[key].get('text', '')
+ imagen = self.options[key].get('img', '')
+
+ if text: check.set_label(text)
+
+ if imagen:
+ loader = GdkPixbuf.PixbufLoader()
+ image_string = base64.b64decode(imagen)
+ loader.write(image_string)
+ loader.close()
- self.pack_start(check, False, False, 3)
+ pixbuf = loader.get_pixbuf().scale_simple(
+ 80, 80, GdkPixbuf.InterpType.BILINEAR)
+
+ check.set_image(Gtk.Image.new_from_pixbuf(pixbuf))
+
+ self.attach_defaults(check,
+ col, col+1,
+ row, row+1)
+ col += 1
+
+ if col == columns:
+ col = 0
+ row += 1
+
check.connect('toggled', self.change)
-
+
self.show_all()
def change(self, widget):
"""
Cuando el usuario hace click sobre una opción,
- se emite la señal new con los datos para todas las opciones.
+ se checkean dependencias y se guardan los datos.
+ """
- Traduce opciones de:
+ if self.updating: return
- 'options': {
- '00001': {'text': 'opcion 1'},
- '00002': {'text': 'opcion 2'}
- }
+ options = self.get_children()
+
+ desactivan = []
+ activan = []
+
+ for child in options:
- a:
+ if child.get_active():
+ activan.append(child.indice)
+
+ elif not child.get_active():
+ desactivan.append(child.indice)
+
+ self.emit('new', activan, desactivan)
- 'options': {
- '00001': {Opcion: valor},
- '00002': {Opcion: valor}
- }
+ def update(self, dict):
+ """
+ Cuando el usuario cambia de encuestado,
+ actualiza sus respuestas en la pregunta.
"""
- keys = self.options.keys()
- keys.sort()
-
- new_dict = {}
+ self.updating = True
options = self.get_children()
for child in options:
- indice = options.index(child)
-
- new_dict[keys[indice]] = {
- child.get_label(): child.get_active()}
-
- self.emit('new', new_dict)
+ if child.indice in dict['default']:
+ child.set_active(True)
+
+ else:
+ child.set_active(False)
+
+ self.updating = False
+
+class MyCheckButton(Gtk.CheckButton):
+
+ def __init__(self, indice):
- def update(self, dict):
+ Gtk.CheckButton.__init__(self)
+
+ self.indice = indice
+
+ self.show_all()
+
+class MyRadioButton(Gtk.RadioButton):
+
+ def __init__(self, indice):
+
+ Gtk.RadioButton.__init__(self)
+
+ self.indice = indice
+
+ self.show_all()
+
+class My_Alert_Dialog(Gtk.Dialog):
+ """
+ Dialogo que alerta al usuario sobre
+ una acción que obliga a perder los datos.
+ """
+
+ def __init__(self, parent_window = None, label = ""):
+
+ Gtk.Dialog.__init__(
+ self, title = "ATENCION !",
+ parent = parent_window,
+ flags = Gtk.DialogFlags.MODAL,
+ buttons = [
+ "Proceder", Gtk.ResponseType.ACCEPT,
+ "Cancelar", Gtk.ResponseType.CANCEL])
+
+ label = Gtk.Label(label)
+ label.show()
+
+ self.vbox.pack_start(label, True, True, 0)
+
+class ToolbarEncuesta(Gtk.Toolbar):
+
+ __gsignals__ = {
+ "accion":(GObject.SIGNAL_RUN_FIRST,
+ GObject.TYPE_NONE, (GObject.TYPE_STRING,))}
+
+ def __init__(self):
+
+ Gtk.Toolbar.__init__(self)
+
+ separador = Gtk.SeparatorToolItem()
+ separador.props.draw = False
+ separador.set_size_request(0, -1)
+ separador.set_expand(True)
+
+ self.insert(separador, -1)
+
+ item = Gtk.ToolItem()
+ item.set_expand(False)
+ button = Gtk.Button("Pag. Anterior")
+ button.set_tooltip_text("Ir a la Página de Preguntas Anterior")
+ button.connect("clicked", self.__button_clicked)
+ button.show()
+ item.add(button)
+ self.insert(item, -1)
+
+ item = Gtk.ToolItem()
+ item.set_expand(False)
+ button = Gtk.Button("Pag. Siguiente")
+ button.set_tooltip_text("Ir a la Siguiente Página de Preguntas")
+ button.connect("clicked", self.__button_clicked)
+ button.show()
+ item.add(button)
+ self.insert(item, -1)
+
+ self.show_all()
+
+ def __button_clicked(self, widget):
- pass
- # print "Widget_MultipleCheckBox", dict \ No newline at end of file
+ self.emit('accion', widget.get_label())
+ \ No newline at end of file
diff --git a/webapp/README b/webapp/README
index 2a75dd5..4121de7 100644
--- a/webapp/README
+++ b/webapp/README
@@ -1,5 +1,8 @@
=== dev env ===
+system requirements:
+- libjpeg
+
python version: 2.7.3
1 - create virtualenv (example: mkvirtualenv --no-site-packages polls)
diff --git a/webapp/webapp/media/output/empty b/webapp/accounts/__init__.py
index e69de29..e69de29 100644
--- a/webapp/webapp/media/output/empty
+++ b/webapp/accounts/__init__.py
diff --git a/webapp/accounts/templates/login.html b/webapp/accounts/templates/login.html
new file mode 100644
index 0000000..844be86
--- /dev/null
+++ b/webapp/accounts/templates/login.html
@@ -0,0 +1,50 @@
+{% extends "base-main-public.html" %}
+{% load i18n %}
+
+{% block main_container %}
+
+ <div class="well">
+ <form method="post" action="{% url accounts:login %}">{% csrf_token %}
+
+ <fieldset>
+ <legend>{% trans "Login" %}</legend>
+
+ {% if form.errors and not form.errors.password and not form.errors.username %}
+ <div class="control-group error">
+ <div class="controls">
+ <span class="help-inline">
+ {% for error in form.errors.values %}
+ {{ error }}
+ {% endfor %}
+ </span>
+ </div>
+ </div>
+ {% endif %}
+
+ <div class="control-group {% if form.errors.username %}error{% endif %}">
+ <label>{{ form.username.label }}</label>
+ <div class="controls">
+ {{ form.username }}
+ <span class="help-inline">
+ {% if form.errors.username %}{{ form.errors.username }}{% endif %}
+ </span>
+ </div>
+ </div>
+
+ <div class="control-group {% if form.errors.password %}error{% endif %}">
+ <label>{{ form.password.label }}</label>
+ <div class="controls">
+ {{ form.password }}
+ <span class="help-inline">{% if form.errors.password %}{{ form.errors.password }}{% endif %}</span>
+ </div>
+ </div>
+
+ <input type="hidden" name="next" value="{{ next }}" />
+
+ <button type="submit" class="btn">{% trans "login" %}</button>
+
+ </fieldset>
+ </form>
+ </div>
+
+{% endblock %} \ No newline at end of file
diff --git a/webapp/accounts/urls.py b/webapp/accounts/urls.py
new file mode 100644
index 0000000..1a5c212
--- /dev/null
+++ b/webapp/accounts/urls.py
@@ -0,0 +1,14 @@
+from django.conf.urls.defaults import *
+
+
+urlpatterns = patterns('',
+ # login, logout
+ url(
+ r'^login/', 'django.contrib.auth.views.login',
+ {'template_name': "login.html",},
+ name='login'
+ ),
+ url(r'^logout/$', 'django.contrib.auth.views.logout',
+ {'next_page': '/'}, name="logout"
+ ),
+) \ No newline at end of file
diff --git a/webapp/webapp/media/output/empty b/webapp/custom_admin/__init__.py
index e69de29..e69de29 100644
--- a/webapp/webapp/media/output/empty
+++ b/webapp/custom_admin/__init__.py
diff --git a/webapp/custom_admin/admin.py b/webapp/custom_admin/admin.py
new file mode 100644
index 0000000..27840f3
--- /dev/null
+++ b/webapp/custom_admin/admin.py
@@ -0,0 +1,16 @@
+from django.utils.translation import ugettext_lazy as _
+from django.contrib import admin
+from django.contrib.auth.models import User, Group
+from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
+
+
+class UserAdmin(DjangoUserAdmin):
+ fieldsets = (
+ (None, {'fields': ('username', 'password')}),
+ (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}),
+ (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
+ )
+
+admin.site.unregister(Group)
+admin.site.unregister(User)
+admin.site.register(User, UserAdmin)
diff --git a/webapp/custom_admin/fixtures/initial_data.json b/webapp/custom_admin/fixtures/initial_data.json
new file mode 100644
index 0000000..791ebb2
--- /dev/null
+++ b/webapp/custom_admin/fixtures/initial_data.json
@@ -0,0 +1,20 @@
+[
+ {
+ "pk": 1,
+ "model": "auth.user",
+ "fields": {
+ "username": "admin",
+ "first_name": "",
+ "last_name": "",
+ "is_active": true,
+ "is_superuser": true,
+ "is_staff": true,
+ "last_login": "2013-03-12T17:49:51.425Z",
+ "groups": [],
+ "user_permissions": [],
+ "password": "pbkdf2_sha256$10000$C9OQ9e4zFz8T$4OIkgqBuNsA3pgjDQYAlYO1jMvVGTzIexodaGHcmou8=",
+ "email": "admin@admin.com",
+ "date_joined": "2013-03-12T15:58:36.539Z"
+ }
+ }
+] \ No newline at end of file
diff --git a/webapp/webapp/media/output/empty b/webapp/custom_admin/models.py
index e69de29..e69de29 100644
--- a/webapp/webapp/media/output/empty
+++ b/webapp/custom_admin/models.py
diff --git a/webapp/custom_admin/urls.py b/webapp/custom_admin/urls.py
new file mode 100644
index 0000000..af64268
--- /dev/null
+++ b/webapp/custom_admin/urls.py
@@ -0,0 +1,10 @@
+from django.conf.urls.defaults import patterns, include, url
+from django.contrib import admin
+
+
+admin.autodiscover()
+
+
+urlpatterns = patterns('',
+ url(r'^', include(admin.site.urls)),
+)
diff --git a/webapp/webapp/media/output/empty b/webapp/db/empty
index e69de29..e69de29 100644
--- a/webapp/webapp/media/output/empty
+++ b/webapp/db/empty
diff --git a/webapp/deploy/dev_testing/nginx.conf b/webapp/deploy/dev_testing/nginx.conf
new file mode 100644
index 0000000..d36338a
--- /dev/null
+++ b/webapp/deploy/dev_testing/nginx.conf
@@ -0,0 +1,24 @@
+server {
+ listen 8002;
+ server_name build.activitycentral.com;
+ access_log /var/log/nginx/polls-dev_testing.net_access.log;
+ error_log /var/log/nginx/polls-dev_testing.net_error.log;
+ root /home/ceibal/virtualenvs/polls/dev_testing/webapp/webapp;
+
+ location / {
+ uwsgi_pass unix:///var/run/uwsgi/app/polls_webapp_dev_testing/socket;
+ include uwsgi_params;
+ }
+
+ location /media/ {
+ alias /home/ceibal/virtualenvs/polls/dev_testing/webapp/webapp/media/;
+ }
+
+ location /static/ {
+ alias /home/ceibal/virtualenvs/polls/dev_testing/webapp/webapp/static/;
+ }
+
+ location /favicon.ico {
+ alias /home/ceibal/virtualenvs/polls/dev_testing/webapp/webapp/static/favicon.ico;
+ }
+}
diff --git a/webapp/deploy/dev_testing/settings.py b/webapp/deploy/dev_testing/settings.py
new file mode 100644
index 0000000..d35dd10
--- /dev/null
+++ b/webapp/deploy/dev_testing/settings.py
@@ -0,0 +1,25 @@
+from utils.mongo_connection import register_connection
+
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+# Hosts/domain names that are valid for this site; required if DEBUG is False
+# See https://docs.djangoproject.com/en//ref/settings/#allowed-hosts
+ALLOWED_HOSTS = ['.activitycentral.com']
+
+MONGO_SETTINGS = {
+ 'ALIAS': 'default',
+ 'NAME': 'dev_testing_polls',
+ 'USER': '',
+ 'PASSWORD': '',
+ 'HOST': '127.0.0.1',
+ 'PORT': 27017,
+}
+
+register_connection(
+ alias=MONGO_SETTINGS['ALIAS'],
+ name=MONGO_SETTINGS['NAME'],
+ host=MONGO_SETTINGS['HOST'],
+ port=MONGO_SETTINGS['PORT'],
+)
diff --git a/webapp/deploy/dev_testing/uwsgi.ini b/webapp/deploy/dev_testing/uwsgi.ini
new file mode 100644
index 0000000..dcac97d
--- /dev/null
+++ b/webapp/deploy/dev_testing/uwsgi.ini
@@ -0,0 +1,11 @@
+[uwsgi]
+vhost = true
+plugins = python
+socket = /run/uwsgi/app/polls_webapp_dev_testing/socket
+master = true
+enable-threads = true
+processes = 2
+wsgi-file = /home/ceibal/virtualenvs/polls/dev_testing/webapp/webapp/wsgi.py
+virtualenv = /home/ceibal/virtualenvs/polls/dev_testing
+chdir = /home/ceibal/virtualenvs/polls/dev_testing/webapp
+touch-reload = /home/ceibal/virtualenvs/polls/dev_testing/reload \ No newline at end of file
diff --git a/webapp/deploy/fabfile.py b/webapp/deploy/fabfile.py
index bcf185d..b23f16b 100644
--- a/webapp/deploy/fabfile.py
+++ b/webapp/deploy/fabfile.py
@@ -2,6 +2,7 @@ from __future__ import with_statement
from functools import wraps
from fabric.api import run, cd, task, env, hide, prefix, sudo
+from fabric.context_managers import settings
class NoDbName(BaseException):
@@ -78,8 +79,9 @@ class deploy(object):
run("git clone --bare -b %s %s repo" % (self.branch, git_url))
with cd(self.virtualenv_path + "/repo"):
run(
- "git archive --format=tar %s %s |"
+ "git archive --format=tar %s %s | "
"tar --exclude='webapp/webapp/media' "
+ "--exclude='webapp/db' "
"-xvf - -C %s" % (self.branch, self.directory, where)
)
run("rm -rf repo")
@@ -117,7 +119,26 @@ class deploy(object):
)
self._set_config()
- sudo("chown -R www-data:ceibal webapp/webapp/media/")
+ activate = 'source %s/bin/activate' % self.virtualenv_path
+
+ # Ensure media directory
+ run('mkdir -p webapp/webapp/media')
+
+ # Ensure db directory
+ run('mkdir -p webapp/db')
+
+ # Sync index for mongo db
+ with prefix(activate):
+ run('python webapp/manage.py syncindex')
+
+ with settings(warn_only=True):
+ sudo("chown -R www-data:ceibal webapp/webapp/media/")
+ sudo("chown -R www-data:ceibal webapp/db")
+ sudo("chmod -R 770 webapp/db")
+
+ # Django syncdb
+ with prefix(activate):
+ run('python webapp/manage.py syncdb --noinput')
# Configure webserver configuration
show_message("Copying webserver and wsgi configuration files")
@@ -138,25 +159,32 @@ def update_packages():
pip_install(env.virtualenv_path)
-# @task(alias="dev_testing")
-# @deploy("dev_testing", "dev")
-# def dev_testing(*args, **kwargs):
-# web_server_reload()
+# For deploy a tag version, e.g.: fab staging:v1.0
+# For deploy another branch, e.g.: fab staging:branch_name
+
+# For use by the development team, no QA, debug=True.
+@task(alias="dev_testing")
+@deploy("dev_testing", "DEV")
+def dev_testing(*args, **kwargs):
+ web_server_reload()
+# Purpose of showing specific functionality to end user before master.
# @task(alias="dev_staging")
# @deploy("dev_staging", "dev")
# def dev_staging(*args, **kwargs):
# web_server_reload()
-# @task(alias="testing")
-# @deploy("testing", "master")
-# def testing(*args, **kwargs):
-# web_server_reload()
+# For QA testing, debug=True.
+@task(alias="testing")
+@deploy("testing", "master")
+def testing(*args, **kwargs):
+ web_server_reload()
+# Purpose of end user testing before production.
@task(alias="staging")
-@deploy("staging", "DEV")
+@deploy("staging", "master")
def staging(*args, **kwargs):
web_server_reload()
diff --git a/webapp/deploy/staging/nginx.conf b/webapp/deploy/staging/nginx.conf
index 11a8d70..a5c53a5 100644
--- a/webapp/deploy/staging/nginx.conf
+++ b/webapp/deploy/staging/nginx.conf
@@ -5,10 +5,6 @@ server {
error_log /var/log/nginx/polls-staging.net_error.log;
root /home/ceibal/virtualenvs/polls/staging/webapp/webapp;
- location /media/output {
- autoindex on;
- }
-
location / {
uwsgi_pass unix:///var/run/uwsgi/app/polls_webapp_staging/socket;
include uwsgi_params;
diff --git a/webapp/deploy/staging/settings.py b/webapp/deploy/staging/settings.py
index 524415c..ad9fbd9 100644
--- a/webapp/deploy/staging/settings.py
+++ b/webapp/deploy/staging/settings.py
@@ -1,6 +1,25 @@
+from utils.mongo_connection import register_connection
+
+
DEBUG = False
TEMPLATE_DEBUG = DEBUG
# Hosts/domain names that are valid for this site; required if DEBUG is False
# See https://docs.djangoproject.com/en//ref/settings/#allowed-hosts
-ALLOWED_HOSTS = ['.activitycentral.com'] \ No newline at end of file
+ALLOWED_HOSTS = ['.activitycentral.com']
+
+MONGO_SETTINGS = {
+ 'ALIAS': 'default',
+ 'NAME': 'staging_polls',
+ 'USER': '',
+ 'PASSWORD': '',
+ 'HOST': '127.0.0.1',
+ 'PORT': 27017,
+}
+
+register_connection(
+ alias=MONGO_SETTINGS['ALIAS'],
+ name=MONGO_SETTINGS['NAME'],
+ host=MONGO_SETTINGS['HOST'],
+ port=MONGO_SETTINGS['PORT'],
+)
diff --git a/webapp/deploy/testing/nginx.conf b/webapp/deploy/testing/nginx.conf
new file mode 100644
index 0000000..8c5b45e
--- /dev/null
+++ b/webapp/deploy/testing/nginx.conf
@@ -0,0 +1,24 @@
+server {
+ listen 8001;
+ server_name build.activitycentral.com;
+ access_log /var/log/nginx/polls-testing.net_access.log;
+ error_log /var/log/nginx/polls-testing.net_error.log;
+ root /home/ceibal/virtualenvs/polls/testing/webapp/webapp;
+
+ location / {
+ uwsgi_pass unix:///var/run/uwsgi/app/polls_webapp_testing/socket;
+ include uwsgi_params;
+ }
+
+ location /media/ {
+ alias /home/ceibal/virtualenvs/polls/testing/webapp/webapp/media/;
+ }
+
+ location /static/ {
+ alias /home/ceibal/virtualenvs/polls/testing/webapp/webapp/static/;
+ }
+
+ location /favicon.ico {
+ alias /home/ceibal/virtualenvs/polls/testing/webapp/webapp/static/favicon.ico;
+ }
+}
diff --git a/webapp/deploy/testing/settings.py b/webapp/deploy/testing/settings.py
new file mode 100644
index 0000000..d933ee2
--- /dev/null
+++ b/webapp/deploy/testing/settings.py
@@ -0,0 +1,25 @@
+from utils.mongo_connection import register_connection
+
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+# Hosts/domain names that are valid for this site; required if DEBUG is False
+# See https://docs.djangoproject.com/en//ref/settings/#allowed-hosts
+ALLOWED_HOSTS = ['.activitycentral.com']
+
+MONGO_SETTINGS = {
+ 'ALIAS': 'default',
+ 'NAME': 'testing_polls',
+ 'USER': '',
+ 'PASSWORD': '',
+ 'HOST': '127.0.0.1',
+ 'PORT': 27017,
+}
+
+register_connection(
+ alias=MONGO_SETTINGS['ALIAS'],
+ name=MONGO_SETTINGS['NAME'],
+ host=MONGO_SETTINGS['HOST'],
+ port=MONGO_SETTINGS['PORT'],
+)
diff --git a/webapp/deploy/testing/uwsgi.ini b/webapp/deploy/testing/uwsgi.ini
new file mode 100644
index 0000000..563432b
--- /dev/null
+++ b/webapp/deploy/testing/uwsgi.ini
@@ -0,0 +1,11 @@
+[uwsgi]
+vhost = true
+plugins = python
+socket = /run/uwsgi/app/polls_webapp_testing/socket
+master = true
+enable-threads = true
+processes = 2
+wsgi-file = /home/ceibal/virtualenvs/polls/testing/webapp/webapp/wsgi.py
+virtualenv = /home/ceibal/virtualenvs/polls/testing
+chdir = /home/ceibal/virtualenvs/polls/testing/webapp
+touch-reload = /home/ceibal/virtualenvs/polls/testing/reload \ No newline at end of file
diff --git a/webapp/js_tests/files.json b/webapp/js_tests/files.json
new file mode 100644
index 0000000..f7e5203
--- /dev/null
+++ b/webapp/js_tests/files.json
@@ -0,0 +1,11 @@
+{
+ "name": "Main Test Suite",
+ "js_files": [
+ "/static/js/jquery.min.js",
+ "/static/js/jquery-ui.js",
+ "/static/js/mustache-0.7.0.js",
+ "/static/js/dynamic_structure.js"
+ ],
+ "media_files": [
+ ]
+} \ No newline at end of file
diff --git a/webapp/js_tests/fixtures/container.html b/webapp/js_tests/fixtures/container.html
new file mode 100644
index 0000000..321a865
--- /dev/null
+++ b/webapp/js_tests/fixtures/container.html
@@ -0,0 +1,12 @@
+<button id="WGroup_add" class="btn btn-primary">
+ <i class="icon-white icon-add"></i>&nbsp;'Agregar grupo'
+</button>
+
+<div id="WGroupContainer"></div>
+
+<script>
+ var container = $('#WGroupContainer'),
+ WIDGET_TYPES = [{'0': {'key': 'TextInput', 'value': 'Respuesta de texto'}}],
+ WITH_OPTIONS = ["MultipleCheckBox", "DropDownList", "RadioButton"],
+ OFFSET_OPTION_ID = 1;
+</script> \ No newline at end of file
diff --git a/webapp/js_tests/spec/DynamicStructureSpec.js b/webapp/js_tests/spec/DynamicStructureSpec.js
new file mode 100644
index 0000000..ed09af0
--- /dev/null
+++ b/webapp/js_tests/spec/DynamicStructureSpec.js
@@ -0,0 +1,204 @@
+describe("", function() {
+
+ jasmine.getFixtures().fixturesPath = "fixtures/";
+
+ it("Correct container", function() {
+ loadFixtures("container.html");
+
+ var container = $('#WGroupContainer');
+
+ expect(container).toExist();
+ expect(container).toBeEmpty();
+ });
+
+ describe("Groups", function() {
+
+ beforeEach(function() {
+
+ jasmine.getFixtures().fixturesPath = "fixtures/";
+ loadFixtures("container.html");
+
+ var group_mustache_template = $.ajax({
+ type: "GET",
+ url: '/jasmine/mustache_templates/group.html',
+ cache: false,
+ async: false
+ }).responseText;
+ $("#jasmine-fixtures").append(group_mustache_template);
+
+ var group_mustache_template = $.ajax({
+ type: "GET",
+ url: '/jasmine/mustache_templates/field.html',
+ cache: false,
+ async: false
+ }).responseText;
+ $("#jasmine-fixtures").append(group_mustache_template);
+
+ var group_mustache_template = $.ajax({
+ type: "GET",
+ url: '/jasmine/mustache_templates/option_default.html',
+ cache: false,
+ async: false
+ }).responseText;
+ $("#jasmine-fixtures").append(group_mustache_template);
+
+ Mustache.tags = ['[[', ']]'];
+
+ // Preparing mustache TEMPLATES
+ $('script[type="text/x-mustache-template"]').each(function(i, obj){
+ TEMPLATES[$(obj).attr('name')] = $(obj).text();
+ });
+
+ });
+
+ it("- Group template must contains right classes", function() {
+ var group_template = $(TEMPLATES['group']);
+
+ var group_remove_button = group_template.find('.WGroup_remove');
+ expect(group_remove_button).toExist();
+ expect(group_remove_button).toBe(':button');
+
+ var group_add_button = group_template.find('.WGroup_add_field');
+ expect(group_add_button).toExist();
+ expect(group_add_button).toBe(':button');
+
+ expect(group_template.find('.error')).toExist();
+
+ expect(group_template.find('.WGroup_field_containter')).toExist();
+ });
+
+ it("- factoryGroup must add a empty group in WGroupContainer", function() {
+ var groups = $('#WGroupContainer > .group');
+
+ expect(groups.length).toBe(0);
+
+ factoryGroup(0, {"name": '', "fields": []});
+
+ groups = $('#WGroupContainer > .group');
+
+ expect(groups.length).toBe(1);
+ });
+
+ it("- Render group widget: A new group must contain right names for inputs", function() {
+ factoryGroup(0, {"name": '', "fields": []});
+
+ var inputs = $('.group').find(':input:not(button)');
+
+ $.each(inputs, function(i, input) {
+ expect($(input).attr('name')).toContain("groups.0.");
+ });
+
+ });
+
+ it("- Render group widget: show name", function() {
+ factoryGroup(0, {"name": 'group name', "fields": []});
+
+ var input_name = $(':input[name="groups.0.name"]');
+
+ expect(input_name.attr('value')).toBe('group name');
+ });
+
+ it("- Render group widget: show error", function() {
+ factoryGroup(0, {"name": '', "fields": [], 'errors': ['error#1']});
+
+ var error = $('.error li');
+
+ expect(error).toHaveText('error#1');
+ });
+
+ it("- Render fields", function() {
+ factoryGroup(0,
+ {"name": '', "fields": [{'widget_type': "InputText"}]}
+ );
+
+ fields = $('.field');
+
+ expect(fields.length).toBe(1);
+
+ });
+
+ it("- bindGroupRemoveButton: Remove group and Update order of groups, very important!", function() {
+ var groups;
+
+ factoryGroup(0, {"name": 'group name', "fields": []});
+
+ var group_widget_0 = $(".group");
+ var button_remove_group_widget_0 = group_widget_0.find('.WGroup_remove');
+
+ factoryGroup(1, {"name": 'group name', "fields": []});
+
+ groups = $('#WGroupContainer > .group');
+
+ expect(groups.length).toBe(2);
+
+ button_remove_group_widget_0.click();
+
+ groups = $('#WGroupContainer > .group');
+
+ expect(groups.length).toBe(1);
+
+ expect($('.group').has($(':input[name="groups.0.order"]'))).toExist();
+
+ });
+
+ it("- Adding a empty field with right group order", function(){
+ var group_order = 0;
+ factoryGroup(group_order, {'name': ''});
+ var add_field_button = $('.group').find('.WGroup_add_field');
+
+ add_field_button.click();
+
+ var fields = $('.field');
+ expect(fields.length).toBe(1);
+
+ var inputs = $(fields[0]).find(':input:not(button)');
+ $.each(inputs, function(i, input) {
+ expect($(input).attr('name')).toContain("groups.0.fields.");
+ });
+ });
+
+ });
+
+ describe("Fields", function() {
+
+ beforeEach(function() {
+
+ jasmine.getFixtures().fixturesPath = "fixtures/";
+ loadFixtures("container.html");
+
+ var group_mustache_template = $.ajax({
+ type: "GET",
+ url: '/jasmine/mustache_templates/field.html',
+ cache: false,
+ async: false
+ }).responseText;
+ $("#jasmine-fixtures").append(group_mustache_template);
+
+ Mustache.tags = ['[[', ']]'];
+
+ // Preparing mustache TEMPLATES
+ $('script[type="text/x-mustache-template"]').each(function(i, obj){
+ TEMPLATES[$(obj).attr('name')] = $(obj).text();
+ });
+
+ });
+
+ it("- Field template must contains right classes", function() {
+ var field_template = $(TEMPLATES['field']);
+
+ var field_remove_button = field_template.find('.WField_remove');
+ expect(field_remove_button).toExist();
+ expect(field_remove_button).toBe(':button');
+
+ expect(field_template.find('.error')).toExist();
+
+ expect(field_template.find('.WFieldWidgetType')).toExist();
+
+ expect(field_template.find('.WFieldAddOptionButton_container')).toExist();
+
+ expect(field_template.find('.WFieldOptions_container')).toExist();
+ });
+
+ });
+
+}); \ No newline at end of file
diff --git a/webapp/polls/exceptions.py b/webapp/polls/exceptions.py
index 15e676c..76e1fa0 100644
--- a/webapp/polls/exceptions.py
+++ b/webapp/polls/exceptions.py
@@ -1,2 +1,6 @@
class ValidationError(Exception):
pass
+
+
+class UniqueNameError(Exception):
+ pass
diff --git a/webapp/polls/forms.py b/webapp/polls/forms.py
new file mode 100644
index 0000000..45f2aa5
--- /dev/null
+++ b/webapp/polls/forms.py
@@ -0,0 +1,58 @@
+import re
+
+from django import forms
+
+from utils.forms import BadFormValidation
+from utils.mongo_connection import get_db
+
+from polls.models import Poll
+
+
+class PollAddForm(forms.Form):
+ id = forms.RegexField(
+ required=False, regex=r'[0-9A-Fa-f]{24}', widget=forms.HiddenInput())
+ name = forms.CharField(
+ required=True, label="Nombre")
+ status = forms.ChoiceField(
+ required=False,
+ choices=[c for c in Poll.status_choices()] + [('', '')],
+ label="Estado")
+
+ def clean_name(self):
+ id = self.cleaned_data['id']
+ name = self.cleaned_data['name']
+
+ if not id:
+ existing = get_db().polls.find_one(
+ {'name': re.compile("^%s$" % name, re.IGNORECASE)})
+ if existing:
+ msg = u"Poll name '%s' already in use." % name
+ raise forms.ValidationError(msg)
+
+ return name
+
+ def clean_status(self):
+ # Preserving the state. REFACTORING: Idea to preserve in any document
+ # The refactoring is big, django form must be capable that accept
+ # any type of mongo document
+
+ id = self.cleaned_data['id']
+ status = self.cleaned_data['status']
+
+ if id and not status:
+ poll = Poll.get(id)
+ status = poll.status
+
+ return status
+
+ def save(self):
+ # require: run is_valid method first
+ poll = Poll(data=self.cleaned_data)
+
+ try:
+ poll_id = poll.save()
+ return Poll.get(id=poll_id)
+ except Poll.ValidationError:
+ raise BadFormValidation(poll.errors)
+
+ return None
diff --git a/webapp/webapp/media/output/empty b/webapp/polls/management/__init__.py
index e69de29..e69de29 100644
--- a/webapp/webapp/media/output/empty
+++ b/webapp/polls/management/__init__.py
diff --git a/webapp/webapp/media/output/empty b/webapp/polls/management/commands/__init__.py
index e69de29..e69de29 100644
--- a/webapp/webapp/media/output/empty
+++ b/webapp/polls/management/commands/__init__.py
diff --git a/webapp/polls/management/commands/syncindex.py b/webapp/polls/management/commands/syncindex.py
new file mode 100644
index 0000000..36c21c7
--- /dev/null
+++ b/webapp/polls/management/commands/syncindex.py
@@ -0,0 +1,18 @@
+from django.core.management.base import BaseCommand
+
+from utils.mongo_connection import get_db, ConnectionError
+
+
+class Command(BaseCommand):
+
+ def handle(self, *args, **options):
+ try:
+ print "Getting db connection..."
+ db = get_db()
+ print "Db connection sucess: %s" % db.name
+ except ConnectionError, e:
+ if "[Errno 61]" in str(e):
+ print "Connection refused: Ensure that mongod is running"
+ else:
+ print " - Ensure unique name key in poll collection"
+ db.polls.ensure_index('name', unique=True)
diff --git a/webapp/polls/models.py b/webapp/polls/models.py
index f2b9119..19a8d3c 100644
--- a/webapp/polls/models.py
+++ b/webapp/polls/models.py
@@ -1,7 +1,21 @@
# -*- encoding: utf-8 -*-
import time
+import json
+import os
+import shutil
+import re
+import Image
+import base64
from exceptions import *
+from bson import ObjectId, DBRef
+
+from django.conf import settings
+from django.forms.fields import ImageField
+from django.core.exceptions import ValidationError as DjangoValidationError
+from django.core.files.uploadedfile import InMemoryUploadedFile
+
+from utils.mongo_connection import get_db
WIDGET_TYPES = (
@@ -9,40 +23,251 @@ WIDGET_TYPES = (
('MultipleCheckBox', 'Respuesta con checks (multiple selección)'),
('RadioButton', 'Respuesta con radios (Solo una selección)'),
('DropDownList', 'Respuesta con lista de opciones'),
+ ('ImageCheckBox', 'Respuesta con imagenes tipo checks'),
+ ('ImageRadioButton', 'Respuesta con imagenes tipo radios'),
)
-class AbastractObject(object):
+WITH_OPTIONS = [
+ "MultipleCheckBox",
+ "DropDownList",
+ "RadioButton",
+ "ImageCheckBox",
+ "ImageRadioButton",
+]
+
+
+WITH_IMAGES = ["ImageCheckBox", "ImageRadioButton"]
+
+
+class ComponentStructure(ObjectId):
+
+ def __init__(self, poll=None, *args, **kwargs):
+ super(ComponentStructure, self).__init__(*args, **kwargs)
+ self.poll = poll
+
+
+class AbstracErrorObject(object):
ValidationError = ValidationError
errors = []
+ dict_errors = {}
+
+
+class Poll(AbstracErrorObject):
+
+ collection_name = 'polls'
+ UniqueNameError = UniqueNameError
+
+ OPEN = "Abierta"
+ CLOSED = "Cerrada"
+
+ def __init__(self, data={}, *args, **kwargs):
+ super(Poll, self).__init__(*args, **kwargs)
+ self.id = None
+
+ _id = data.get('id', None) or data.get('_id', None)
+ if _id and (isinstance(_id, str) or isinstance(_id, unicode)):
+ self.id = ObjectId(_id)
+ elif _id and isinstance(_id, ObjectId):
+ self.id = _id
+
+ self.name = data.get('name', None)
+ self.status = data.get('status', Poll.OPEN)
+
+ @property
+ def structure(self):
+ structure_data = get_db().structures.find_one(
+ {'poll.$id': self.id})
+ structure_data = structure_data if structure_data else {}
+
+ return Structure(data=structure_data, poll=self)
+
+ @staticmethod
+ def status_choices():
+ return (
+ (Poll.OPEN, 'Abierta'),
+ (Poll.CLOSED, 'Cerrada'),
+ )
+
+ def is_open(self):
+ return self.status == Poll.OPEN
+
+ def to_dict(self):
+ _dict = {}
+
+ if self.id:
+ _dict.update({'_id': self.id})
+
+ if self.name:
+ _dict.update({'name': self.name})
+
+ if self.status:
+ _dict.update({'status': self.status})
+
+ return _dict
+
+ def validate(self):
+ self.errors = []
+
+ if not self.name:
+ msg = "Necesita ingresar un nombre de encuesta."
+ self.errors.append(msg)
+ else:
+ # Check unique name key, Important !!!
+ existing = get_db().polls.find_one(
+ {'name': re.compile("^%s$" % self.name, re.IGNORECASE)})
+ if existing and existing.get("_id", None) != self.id:
+ msg = u"Poll name '%s' already in use." % self.name
+ self.errors.append(msg)
+ raise Poll.ValidationError(msg)
+
+ if len(self.errors):
+ raise Poll.ValidationError(str(self.errors))
+
+ def save(self):
+ self.validate()
+
+ poll_id = None
+
+ poll_id = get_db().polls.save(self.to_dict())
+
+ return poll_id
+
+ @staticmethod
+ def get(id=None):
+ poll = None
+
+ objects = get_db().polls.find({'_id': ObjectId(id)})
+ if objects.count():
+ obj = objects[0]
+
+ poll = Poll(obj)
+
+ return poll
+
+ # TODO: Test
+ @staticmethod
+ def all(*args, **kwargs):
+ _all = []
+ for poll_data in get_db().polls.find(**kwargs):
+ _all.append(Poll(poll_data))
+
+ return _all
+
+ def to_json(self):
+ structure_data = get_db().structures.find_one(
+ {'poll.$id': self.id}, fields={'poll': False})
+ structure = Structure(structure_data, poll=self)
+
+ _json = json.dumps(
+ structure.to_python(with_errors=False, img_serialize=True),
+ sort_keys=True,
+ indent=4,
+ separators=(',', ': '),
+ ensure_ascii=False
+ )
+
+ return _json
+
+
+class AbstractObject(AbstracErrorObject):
@staticmethod
def get_offset_id():
return int(time.time() * 1000)
-class Option(AbastractObject):
+class Option(AbstractObject, ComponentStructure):
- def __init__(self, id, text):
- super(Option, self).__init__()
+ def __init__(self, data={}, *args, **kwargs):
+ super(Option, self).__init__(*args, **kwargs)
- self.id = id
- self.text = text
+ self.id = data.get('id', None)
+ self.text = data.get('text', None)
+ self.img = data.get('img', None)
+ self.img_name = data.get('img_name', None)
- @staticmethod
- def from_dict(data):
- option = Option(
- id=data.get('id', None),
- text=data.get('text', None),
+ weight = data.get('weight', None)
+ self.weight = int(weight) if weight else weight
+
+ if not self.img_name and isinstance(self.img, InMemoryUploadedFile):
+ fileExtension = os.path.splitext(self.img.name)[1]
+ self.img_name = '%s%s' % (self.id, fileExtension)
+
+ def get_absolute_path(self):
+ return "%s/%s/%s" % (
+ settings.IMAGE_OPTIONS_ROOT, str(self.poll.id), self.img_name
)
- return option
+ def validate(self):
+ self.dict_errors = {}
+ self.errors = []
+
+ if self.img and isinstance(self.img, InMemoryUploadedFile):
+ try:
+ img = ImageField().to_python(self.img)
+ except DjangoValidationError, e:
+ self.dict_errors.update(
+ {'img': '%s: %s' % (self.img.name, e.messages[0])})
+ else:
+ width, height = Image.open(img).size
+ if width > 250 or height > 250:
+ msg = u"Se necesita una imagen menor a 250x250."
+ self.dict_errors.update(
+ {'img': '%s: %s' % (self.id, msg)})
+
+ if 'img' in self.dict_errors.keys():
+ self.img_name = None
+ self.img = None
+ else:
+ self.img.seek(0)
+
+ if self.weight is None or self.weight == '':
+ msg = u"opcion %s: ponderación requerida." % self.id
+ self.dict_errors.update({'weight': msg})
+
+ self.errors = self.dict_errors.values()
+ if len(self.errors):
+ raise Option.ValidationError(str(self.errors))
+
+ def to_python(self, with_errors=False, img_serialize=False):
+
+ data = {'%s' % self.id: {}}
+
+ if self.text:
+ data[self.id].update({'text': self.text})
+
+ if self.img_name:
+ if img_serialize:
+ img_path = self.get_absolute_path()
+
+ img_file = open(img_path, 'rb')
+ image_string = base64.b64encode(img_file.read())
+ img_file.close()
+
+ data[self.id].update({'img': image_string})
+ else:
+ data[self.id].update({'img_name': self.img_name})
+
+ if self.weight is not None and self.weight != '':
+ data[self.id].update({'weight': self.weight})
+
+ return data
-class Field(AbastractObject):
+class Field(AbstractObject, ComponentStructure):
- rules = {
+ TextInput = 'TextInput'
+
+ MultipleCheckBox = 'MultipleCheckBox'
+ RadioButton = 'RadioButton'
+ DropDownList = 'DropDownList'
+
+ ImageCheckBox = 'ImageCheckBox'
+ ImageRadioButton = 'ImageRadioButton'
+
+ VALIDATION_RULES = {
'MultipleCheckBox': (
lambda f: f.options and len(f.options) > 0,
"Respuesta con checks (multiple selección): necesita "
@@ -58,70 +283,65 @@ class Field(AbastractObject):
"Respuesta con lista de opciones: necesita "
"al menos una opción."
),
+ 'ImageCheckBox': (
+ lambda f: f.options and len(f.options) > 0,
+ "Respuesta con checks (multiple selección): necesita "
+ "al menos una imagen de opción."
+ ),
+ 'ImageRadioButton': (
+ lambda f: f.options and len(f.options) > 1,
+ "Respuesta con radios (Solo una selección): necesita "
+ "al menos dos imagenes de opciones."
+ ),
'TextInput': (lambda f: True, ""),
}
- def __init__(self, name, widget_type, key=None):
- super(Field, self).__init__()
+ def __init__(self, data={}, *args, **kwargs):
+ super(Field, self).__init__(*args, **kwargs)
- self.name = name
- self.key = key
- self.options = None
- self.dependence = None
+ order = data.get('order', None)
+ self.order = int(order) if order else order
+ self.name = data.get('name', None)
+ self.dependence = data.get('dependence', None)
+ self.options = []
+ widget_type = data.get('widget_type', None)
if widget_type and widget_type not in dict(WIDGET_TYPES).keys():
raise AttributeError(
- 'valid widget types are TextInput, MultipleCheckBox, \
- RadioButton, DropDownList.'
+ 'valid widget types are %s' % WIDGET_TYPES.keys().join(', ')
)
self.widget_type = widget_type
- @staticmethod
- def from_dict(data):
- name = data.get('name', None)
- key = data.get('key', None)
- widget_type = data.get('widget_type', None)
- dependence = data.get('dependence', None)
-
- field = Field(
- key=key,
- name=name,
- widget_type=widget_type,
- )
-
- if dependence:
- field.add_dependence(dependence)
-
- return field
-
def add_options(self, data):
for id, info in data.iteritems():
opt_data = {'id': id}
opt_data.update(info)
- opt = Option.from_dict(opt_data)
+ opt = Option(opt_data, poll=self.poll)
self.options = self.options if self.options is not None else []
- if opt_data['text'] and opt not in self.options:
+ if opt_data.get('text', None) and opt not in self.options:
self.options.append(opt)
- def add_dependence(self, dependence):
- self.dependence = dependence
+ img = opt_data.get('img', None) or opt_data.get('img_name', None)
+ if img and opt not in self.options:
+ self.options.append(opt)
def validate(self, options=[]):
self.errors = []
- rule, msg = Field.rules.get(self.widget_type)
+ rule, msg = Field.VALIDATION_RULES.get(self.widget_type)
if not rule(self):
self.errors.append(msg)
- options_id = [opt.id for opt in options]
-
- # TODO: Test
- # TODO: Comprobacion que exista en el mismo grupo
- # TODO: Que la dependencia no sea de una opcion de este campo
- if self.dependence and self.dependence not in options_id:
- msg = "Dependencia no valida"
- self.errors.append(msg)
+ # Validate option of current field
+ for opt in self.options:
+ # TODO: Refactoring this.
+ # HORRIBLE path to avoid validation for TextInput options.
+ if self.widget_type != Field.TextInput:
+ try:
+ opt.validate()
+ except Option.ValidationError:
+ self.errors += opt.errors
# TODO: Test
if not self.name:
@@ -131,87 +351,336 @@ class Field(AbastractObject):
if len(self.errors):
raise Field.ValidationError(str(self.errors))
- def need_options(self):
- return self.widget_type in (
- 'MultipleCheckBox', 'RadioButton', 'DropDownList')
+ def to_python(self, with_errors=False, img_serialize=False):
+
+ data = {}
+ data.update({
+ 'name': self.name,
+ 'widget_type': self.widget_type,
+ 'options': {}
+ })
+ if self.dependence:
+ data.update({'dependence': self.dependence})
+
+ if with_errors:
+ data.update({'errors': self.errors})
+
+ options = self.options if self.options else []
+ for option in options:
+ data['options'].update(option.to_python(
+ with_errors=with_errors, img_serialize=img_serialize)
+ )
+ return {'%d' % self.order: data}
-class Group(AbastractObject):
- def __init__(self, name, fields=[]):
- super(Group, self).__init__()
- self.name = name
- self.fields = fields
+class Group(AbstractObject, ComponentStructure):
+
+ def __init__(self, data={}, *args, **kwargs):
+ super(Group, self).__init__(*args, **kwargs)
+
+ order = data.get('order', None)
+ self.order = int(order) if order else order
+ self.name = data.get('name', None)
+ self.fields = data.get('fields', [])
def add_field(self, field, order):
order = int(order)
+ field.order = order
fields_pre = self.fields[:order]
fields_post = self.fields[order:]
self.fields = fields_pre + [field] + fields_post
return field
+ def validate(self):
+ self.errors = []
+
+ if not self.name:
+ msg = "Necesita ingresar un nombre para el grupo."
+ self.errors.append(msg)
+
+ if not self.fields:
+ msg = "Necesita al menos una pregunta para un grupo."
+ self.errors.append(msg)
+
+ if len(self.errors):
+ raise Group.ValidationError(str(self.errors))
+
+ def to_python(self, with_errors=False, img_serialize=False):
+ data = {'name': self.name, 'fields': {}}
+
+ if with_errors:
+ data.update({'errors': self.errors})
+
+ for field_obj in self.fields:
+ field_data = field_obj.to_python(
+ with_errors=with_errors, img_serialize=img_serialize
+ )
+ data['fields'].update(field_data)
+
+ return {'%d' % self.order: data}
-class Structure(object):
+
+class Structure(AbstractObject, ComponentStructure):
"""
{
'groups': {
'0': {
+ 'name': 'group name',
'fields': {
- '0': {'widget_type': 'MultipleCheckBox', 'name': 'sagas'},
- '1': {u'widget_type': 'TextInput', u'name': 'otro'}
+ '0': {
+ 'widget_type': ...,
+ 'name': ...,
+ 'options': ...,
+ 'dependence': ...,
+ },
+ '1' ...
}
},
- '1': {
- 'name': 'nombre'
- 'fields': {
- '0': {'widget_type': 'RadioButton', 'name': 'dsadas'},
- '1': {'widget_type': 'TextInput', 'name': 'asfa'}
- }
- }
+ '1' ...
+ ...
}
}
"""
- def __init__(self, data=None):
- super(Structure, self).__init__()
+ def __init__(self, data=None, poll=None, *args, **kwargs):
+ super(Structure, self).__init__(poll, *args, **kwargs)
self.data = data
self.groups = []
-
+ self.poll = poll
+ self.id = None
+
+ # Getting parent poll id
+ self._poll_id = getattr(poll, 'id', None)
+ if self.data and self._poll_id is None:
+ poll_dbref = data.get('poll', None)
+ poll_id = poll_dbref.id if poll_dbref else None
+ self._poll_id = str(poll_id) if poll_id else self._poll_id
+ self._poll_id = self.data.get('poll_id', self._poll_id)
+
+ # Build model Structure obj based in dict data
if self.data:
+ # Getting id
+ _id = data.get('id', None) or data.get('_id', None)
+ if _id and (isinstance(_id, str) or isinstance(_id, unicode)):
+ self.id = ObjectId(_id)
+ elif _id and isinstance(_id, ObjectId):
+ self.id = _id
+
groups_info = data['groups']
for group_order, group_data in groups_info.iteritems():
- group = Group(name=group_data['name'])
- fields_info = group_data['fields']
+ group = Group({
+ 'order': group_order,
+ 'name': group_data['name']
+ }, poll=self.poll)
+
+ fields_info = group_data.get('fields', {})
for field_order, field_data in fields_info.iteritems():
- field = Field.from_dict(field_data)
+ field_data.update({'order': field_order})
+ field = Field(field_data, poll=self.poll)
field.add_options(field_data.get('options', {}))
- field.add_dependence(field_data.get('dependence', None))
group.add_field(field, field_order)
+
self.add_group(group, group_order)
+ # Require: parent poll id !!!
+ if not self._poll_id:
+ raise Exception('INTERNAL ERROR: A structure need a poll id!')
+
+ @property
+ def poll_id(self):
+ return self._poll_id
+
def add_group(self, group, order):
order = int(order)
+ group.order = order
groups_pre = self.groups[:order]
groups_post = self.groups[order:]
self.groups = groups_pre + [group] + groups_post
return group
- def is_valid(self):
- valid = True
-
- # TODO: corregir este pasaje de opciones forzado
+ def get_options(self):
fields = reduce(
lambda x, y: x + y, [g.fields for g in self.groups], [])
options = reduce(
lambda x, y: x + y, [f.options or [] for f in fields], [])
+ return options
+
+ def get_image_options(self):
+ options = self.get_options()
+ return filter(lambda opt: opt.img_name is not None, options)
+
+ def validate(self):
+ self.errors = []
+
+ if not self.data.get('groups', {}):
+ msg = "Necesita al menos un grupo con preguntas."
+ self.errors.append(msg)
+
+ if len(self.errors):
+ raise Group.ValidationError(str(self.errors))
+
+ def is_valid(self):
+ valid = True
+
+ options = self.get_options()
+
for group in self.groups:
+ try:
+ group.validate()
+ except Group.ValidationError:
+ valid = False
+
for field in group.fields:
try:
field.validate(options)
except Field.ValidationError:
valid = False
+
+ try:
+ self.validate()
+ except Structure.ValidationError:
+ valid = False
+
return valid
+
+ def to_python(self, with_errors=False, img_serialize=False):
+ data = {'groups': {}}
+
+ for group_obj in self.groups:
+ data['groups'].update(
+ group_obj.to_python(
+ with_errors=with_errors, img_serialize=img_serialize
+ )
+ )
+
+ return data
+
+ def save(self):
+ structure_id = None
+
+ self.validate()
+
+ _dict = self.to_python()
+
+ # Prepare dbref to poll object
+ if not self.poll:
+ raise ValidationError("Need a parent poll.")
+ else:
+ dbref = DBRef(Poll.collection_name, ObjectId(self.poll.id))
+ _dict.update({'poll': dbref})
+
+ # Prepare id if is a existing Structure object
+ if self.id:
+ _dict.update({'_id': ObjectId(self.id)})
+
+ # Save process -> Update if it have id, else insert
+ structure_id = get_db().structures.save(_dict)
+
+ # Removing older img options files
+ current_options = self.get_image_options()
+ current_opts_file_name = [opt.img_name for opt in current_options]
+ path = self.get_image_options_path()
+ for file in os.listdir(path):
+ if file not in current_opts_file_name:
+ try:
+ os.remove("%s/%s" % (path, file))
+ except:
+ pass
+
+ return structure_id
+
+ @staticmethod
+ def get(id=None):
+ structure = None
+
+ objects = get_db().structures.find({'_id': ObjectId(id)})
+ if objects.count():
+ obj = objects[0]
+ poll_id = obj['poll'].id
+
+ structure = Structure(obj)
+ structure.poll = Poll.get(poll_id)
+
+ return structure
+
+ def get_image_options_tmp_path(self):
+ path = settings.IMAGE_OPTIONS_ROOT + '/%s/tmp' % self.poll_id
+
+ try:
+ os.makedirs(path)
+ except OSError:
+ pass
+
+ return path
+
+ def get_image_options_path(self):
+ path = settings.IMAGE_OPTIONS_ROOT + '/%s' % self.poll_id
+
+ try:
+ os.makedirs(path)
+ except OSError:
+ pass
+
+ return path
+
+ def get_image_options_tmp_media_url(self):
+ media_url = None
+
+ media_url = settings.IMAGE_OPTIONS_MEDIA_URL + '/%s/tmp' % self.poll_id
+
+ return media_url
+
+ def rollback(self):
+ tmp_path = self.get_image_options_tmp_path()
+ path = self.get_image_options_path()
+ for file in os.listdir(path):
+ if file != "tmp":
+ src_file = os.path.join(path, file)
+ dst_file = os.path.join(tmp_path, file)
+ shutil.move(src_file, dst_file)
+
+ def storing_image_options(self, path, options):
+ for img_opt in options:
+ if isinstance(img_opt.img, InMemoryUploadedFile):
+ with open(path + '/%s' % img_opt.img_name, 'wb+') as dst:
+ for chunk in img_opt.img.chunks():
+ dst.write(chunk)
+ dst.close()
+
+ def save_image_options(self, tmp=False):
+
+ options = self.get_options()
+ valid_img_options = filter(
+ lambda opt: not 'img' in opt.dict_errors.keys(), options)
+
+ tmp_path = self.get_image_options_tmp_path()
+
+ if len(valid_img_options):
+ if tmp:
+ self.storing_image_options(tmp_path, valid_img_options)
+ else:
+ path = self.get_image_options_path()
+ self.storing_image_options(path, valid_img_options)
+
+ # Moving tmp images options to the final place for store them
+ for img_opt in valid_img_options:
+ src = '%s/%s' % (tmp_path, img_opt.img_name)
+ dst = '%s/%s' % (path, img_opt.img_name)
+ try:
+ shutil.move(src, dst)
+ except Exception:
+ # TODO: LOG orphan img options
+ pass
+
+ try:
+ os.removedirs(tmp_path)
+ except:
+ # TODO: tmp must be empty,
+ # if raise exception here is someting bad
+ # TODO: LOG!
+ pass
diff --git a/webapp/polls/templates/base-poll.html b/webapp/polls/templates/base-poll.html
new file mode 100644
index 0000000..0a47444
--- /dev/null
+++ b/webapp/polls/templates/base-poll.html
@@ -0,0 +1,11 @@
+{% extends "base-main.html" %}
+
+{% block messages %}
+ {% if messages %}
+ {% for message in messages %}
+ <div class="alert alert-{{ message.tags }}">
+ {{ message }}
+ </div>
+ {% endfor %}
+ {% endif %}
+{% endblock %} \ No newline at end of file
diff --git a/webapp/polls/templates/builder.html b/webapp/polls/templates/builder.html
deleted file mode 100644
index 81752f2..0000000
--- a/webapp/polls/templates/builder.html
+++ /dev/null
@@ -1,153 +0,0 @@
-{% extends "base-main.html" %}
-{% load i18n poll_tags %}
-
-{% block title %}Builder{% endblock %}
-
-{% block extra_css %}
- <style type="text/css">
- body {
- padding-top: 70px;
- }
- </style>
-{% endblock %}
-
-{% block main_container %}
- <div class="alert alert-info navbar-fixed-top">Para generar dependencias, arrastré el ID de la opción hacia el campo "Depende de la opción". Las dependencias validas se realizan desde una opción de una pregunta de menor orden hacia una pregunta de mayor orden.</div>
-
- <center><h3>Creación de la estructura de encuesta</h3></center>
-
- <form class="form-inline" method="post" action="">
-
- {% for group in structure.groups %}
- <legend>
- <label><h4>Nombre del Grupo:</h4></label>
- <input
- type="text"
- name="groups.{{ forloop.counter0 }}.name"
- value="{{ group.name|default_if_none:'' }}"
- />
- <button class="toggle" group_index="{{ forloop.counter0 }}"><i class="icon-arrow-down"></i>&nbsp;Desplegar campos</button>
- </legend>
-
- <fieldset id="Group{{ forloop.counter0 }}" class="field_area">
-
-
- {% for field in group.fields %}
- {% with group_index=forloop.parentloop.counter0 %}
-
- <div class="well">
-
- {% if field.errors %}
- <div class="control-group error">
- <label class="control-label">
- <ul>
- {% for error in field.errors %}
- <li>{{ error }}</li>
- {% endfor %}
- </ul>
- </label></br>
- </div>
- {% endif %}
-
- <div class="row-fluid">
-
- <label>Depende de la opción:</label>
- <input
- class="input-medium droppable"
- type="text"
- name="groups.{{ group_index }}.fields.{{ forloop.counter0 }}.dependence"
- value="{{ field.dependence|default_if_none:'' }}"
- placeholder="nro. ID"
- _group_index="{{ group_index }}"
- _field_index="{{ forloop.counter0 }}"
- />
-
- {% render_widget_types field %}
-
- </div>
-
- <div class="row-fluid">
-
- <label>Pregunta:</label>
- <input
- class="input-xxlarge"
- type="text"
- name="groups.{{ group_index }}.fields.{{ forloop.counter0 }}.name"
- value="{{ field.name|default_if_none:'' }}"
- placeholder="Pregunta..."
- />
-
- </div>
-
- {% render_options field %}
- </div>
-
- {% endwith %}
- <hr />
- {% endfor %}
-
- </fieldset>
-
- {% endfor %}
-
- <div class="ps-form-toolbar btn-toolbar clearfix">
- <div class="btn-group">
- <button class="btn btn-primary"><i class="icon-white icon-circle-arrow-right"></i>&nbsp;{% trans 'Generar encuesta' %}</button>
- </div>
- </div>
-
- {% csrf_token %}
-
- </form>
-
-<script type="text/javascript">
- (function($){
- $(document).ready(function() {
- $(".field_area").toggle();
- $(".toggle").on('click', function(event) {
- event.preventDefault();
- var group_index = $(this).attr("group_index");
- $("#Group" + group_index).toggle("showOrHide");
-
- /* Hide groups that are visible and have no errors */
- $("fieldset[id!='Group" + group_index +"']:visible")
- .not(":has('.error')")
- .toggle("slow");
- });
-
- /* Keep visible the groups that have errors */
- $('fieldset:has(".error"):hidden').toggle("fast");
-
- });
- })(jQuery);
-</script>
-
-{% endblock %}
-
-{% block main_footer %}
- <hr />
- <div class="alert alert-success">
- <button type="button" class="close" data-dismiss="alert">×</button>
- <h4>Version 1.0</h4>
- <ul>
- <li><p>La dependencia se puede realizar: "Una pregunta depende de una opción de alguna pregunta anterior".</p></li>
- <li><p>Las validaciones en las dependencias (ej: opcion 3 de la pregunta 2, habilita pregunta 3) serán correctas si se usa la opcion de arrastre para generarla, de lo contrario solo validará que sea una opción dentro de los ID existentes.</p></li>
- <li><p>Solo se dispone de 2 grupos y 2 preguntas por grupo.</p></li>
- <li><p>El archivo de generación de encuesta, tendrá como nombre: fecha_hora actual de generación, pero puede ser modificado a la hora del guardado.</p></li>
- <li><p>No existen ningún otro dato relacionado a la encuesta, mas que la generación de su estructura y el salvado de la misma, en un archivo (archivo necesario para la aplicación de llenado de encuestas).</p></li>
- <li>La cantidad de opciones por pregunta, es dinámica, pero se restringe a 4 como maximo.</li>
- </ul>
- </div>
-
- <div class="alert alert-info">
- <button type="button" class="close" data-dismiss="alert">×</button>
- <h4>Version 2.0</h4>
- <ul>
- <li><p>Las dependencias tendrán todas las reglas de validación cuando se ingrese manualmente un ID de opción.</p></li>
- <li><p>Se agregarán y quitarán preguntas y grupos dinamicamente (sin restricción de cantidad).</p></li>
- <li><p>No tendrá restricción la cantidad de opciones por pregunta.</p></li>
- <li><p>Se gestionarán las encuestas que ya se crearon, y se podrá asociar a ellas, nombre, y otros atributos, ademas de la estructura en sí.</p></li>
- <li><p>Se podrá reorganizar el orden en que aparecen las preguntas por grupo, y el orden de los grupos.</p></li>
- </ul>
- </div>
-{% endblock %} \ No newline at end of file
diff --git a/webapp/polls/templates/image_option_thumbnail.html b/webapp/polls/templates/image_option_thumbnail.html
new file mode 100644
index 0000000..d65b169
--- /dev/null
+++ b/webapp/polls/templates/image_option_thumbnail.html
@@ -0,0 +1,7 @@
+{% load thumbnail %}
+
+<div class="thumbnail" style="width: 150px; height: 150px;" >
+ {% thumbnail img "150x150" crop="center" as im %}
+ <img src="{{ im.url }}" img_src="{{ img_src }}" />
+ {% endthumbnail %}
+</div> \ No newline at end of file
diff --git a/webapp/polls/templates/mustache/field.html b/webapp/polls/templates/mustache/field.html
new file mode 100644
index 0000000..3987571
--- /dev/null
+++ b/webapp/polls/templates/mustache/field.html
@@ -0,0 +1,56 @@
+<!-- Template: Field -->
+<script type="text/x-mustache-template" name="field">
+ <div class="row-fluid field well well-small">
+
+ <input type="hidden" name="groups.[[ group_order ]].fields.[[ order ]].order" value="[[ order ]]" class="field_order" />
+
+ [[ #visible_errors ]]
+ <div class="control-group error">
+ <label class="control-label">
+ <ul>
+ [[ #errors ]] <li>[[ error ]]</li> [[ /errors ]]
+ </ul>
+ <label>
+ </div>
+ [[ /visible_errors ]]
+
+ <div class="row-fluid">
+
+ <button class="WField_remove btn btn-danger">
+ <i class="icon-remove-sign icon-white"></i>
+ </button>
+
+ <label><b>Pregunta</b>:</label>
+ <input
+ class="input-xxlarge"
+ type="text"
+ name="groups.[[ group_order ]].fields.[[ order ]].name"
+ value="[[ name ]]"
+ placeholder="Pregunta..." />
+
+ </div>
+
+ <div class="row-fluid">
+ <label><b>Tipo de pregunta</b>:</label>
+ <select class="WFieldWidgetType" name="groups.[[ group_order ]].fields.[[ order ]].widget_type" style="width: 300px;">
+ [[ #WIDGET_TYPES ]]
+ <option value="[[ key ]]" [[ #selected ]]selected="selected"[[ /selected ]]>[[ value ]]</option>
+ [[ /WIDGET_TYPES ]]
+ </select>
+
+ <label><b>Depende de la opci&oacute;n</b>:</label>
+ <input
+ class="input-medium droppable"
+ type="text"
+ name="groups.[[ group_order ]].fields.[[ order ]].dependence"
+ value="[[ dependence ]]"
+ placeholder="nro. ID" />
+
+ </div>
+
+ <div class="WFieldAddOptionButton_container" style="margin: 5px;"></div>
+
+ <div class="WFieldOptions_container well"></div>
+
+ </div>
+</script> \ No newline at end of file
diff --git a/webapp/polls/templates/mustache/group.html b/webapp/polls/templates/mustache/group.html
new file mode 100644
index 0000000..3ea1cde
--- /dev/null
+++ b/webapp/polls/templates/mustache/group.html
@@ -0,0 +1,37 @@
+<!-- Template: Group -->
+<script type="text/x-mustache-template" name="group">
+ <div class="group row-fluid">
+
+ <legend>
+
+ <button class="WGroup_remove btn btn-danger">
+ <i class="icon-remove-sign icon-white"></i>
+ </button>
+
+ <label>
+ <h4>Nombre del Grupo:</h4>
+ </label>
+
+ <input type="text" name="groups.[[ order ]].name" value="[[ name ]]" />
+ <input type="hidden" name="groups.[[ order ]].order" value="[[ order ]]" class="group_order" />
+
+ <button class="WGroup_add_field btn btn-success">
+ <i class="icon-plus-sign icon-white"></i>&nbsp;Agregar pregunta
+ </button>
+
+ [[ #visible_errors ]]
+ <div class="control-group error">
+ <label class="control-label">
+ <ul>
+ [[ #errors ]] <li>[[ error ]]</li> [[ /errors ]]
+ </ul>
+ </label>
+ </div>
+ [[ /visible_errors ]]
+
+ </legend>
+
+ <fieldset class="WGroup_field_containter">
+ </fieldset>
+ </div>
+</script> \ No newline at end of file
diff --git a/webapp/polls/templates/mustache/option.html b/webapp/polls/templates/mustache/option.html
new file mode 100644
index 0000000..0527f05
--- /dev/null
+++ b/webapp/polls/templates/mustache/option.html
@@ -0,0 +1,34 @@
+<!-- Template: Field Option -->
+<script type="text/x-mustache-template" name="field_option">
+ <div class="span6" style="margin-left: 20px">
+ <label style="padding: 5px;">
+ <b>ID</b>: <span class="draggable"><i class="icon-move"></i>[[ id ]]</span>
+ </label>
+ <input
+ class="input-large"
+ type="text"
+ name="groups.[[ group_order ]].fields.[[ field_order ]].options.[[ id ]].text"
+ value="[[ value ]]"
+ placeholder="opci&oacute;n" />
+ <input
+ class="input-small"
+ type="text"
+ name="groups.[[ group_order ]].fields.[[ field_order ]].options.[[ id ]].weight"
+ value="[[ weight ]]"
+ placeholder="peso" />
+ <button class="WFieldOptions_remove btn btn-danger">
+ <i class="icon-trash icon-white"></i>
+ </button>
+ </div>
+</script>
+
+<!-- Template: Add Option button -->
+<script type="text/x-mustache-template" name="field_add_option_button">
+ <div class="WFieldOptions_add_button control-group">
+ <div class="controls">
+ <button class="btn btn-success">
+ <i class="icon-plus-sign icon-white"></i>&nbsp;Agregar opci&oacute;n
+ </button>
+ </div>
+ </div>
+</script> \ No newline at end of file
diff --git a/webapp/polls/templates/mustache/option_default.html b/webapp/polls/templates/mustache/option_default.html
new file mode 100644
index 0000000..130acbf
--- /dev/null
+++ b/webapp/polls/templates/mustache/option_default.html
@@ -0,0 +1,13 @@
+<!-- Template: Field Option default (widget type: RadioButton) -->
+<script type="text/x-mustache-template" name="field_option_default">
+ <label>Texto default:</label>
+ <input
+ class="input-xlarge"
+ type="text"
+ name="groups.[[ group_order ]].fields.[[ field_order ]].options.[[ id ]].text"
+ value="[[ value ]]"
+ placeholder="(opcional)" />
+ <span>
+ Si ingresa un texto default, estar&aacute; escrito en la respuesta a esta pregunta.
+ </span>
+</script> \ No newline at end of file
diff --git a/webapp/polls/templates/mustache/option_image_thumbnail.html b/webapp/polls/templates/mustache/option_image_thumbnail.html
new file mode 100644
index 0000000..43244a7
--- /dev/null
+++ b/webapp/polls/templates/mustache/option_image_thumbnail.html
@@ -0,0 +1,24 @@
+<!-- Template: Field Image option thumbnail -->
+<script type="text/x-mustache-template" name="field_option_image_thumbnail">
+ <div class="span3 WFieldImageOption_container well well-small" style="margin: 10px;">
+ <span class="pull-right">
+ <a href="#" class="WFieldImageOptions_remove_button btn btn-danger">
+ <i class="icon-trash icon-white"></i>
+ </a>
+ </span>
+ <label>
+ <b>ID</b>: <span class="draggable"><i class="icon-move"></i>[[ id ]]</span>
+ </label><br />
+ <center>
+ <input type="hidden" name="groups.[[ group_order ]].fields.[[ field_order ]].options.[[ id ]].img_name" value="[[ img_name ]]" />
+ <div class="img_container"></div>
+ <b>Peso</b>:
+ <input
+ class="input-small"
+ type="text"
+ name="groups.[[ group_order ]].fields.[[ field_order ]].options.[[ id ]].weight"
+ value="[[ weight ]]"
+ placeholder="peso" />
+ </center>
+ </div>
+</script> \ No newline at end of file
diff --git a/webapp/polls/templates/mustache/option_image_upload.html b/webapp/polls/templates/mustache/option_image_upload.html
new file mode 100644
index 0000000..325596d
--- /dev/null
+++ b/webapp/polls/templates/mustache/option_image_upload.html
@@ -0,0 +1,31 @@
+<!-- Template: Field Image option upload -->
+<script type="text/x-mustache-template" name="field_option_image_upload">
+
+ <div class="fileupload fileupload-new span3 well well-small" data-provides="fileupload" style="margin: 10px;">
+ <label>
+ <b>ID</b>: <span class="draggable"><i class="icon-move"></i>[[ id ]]</span>
+ </label><br />
+ <center>
+ <div class="fileupload-new thumbnail" style="width: 150px; height: 150px;">
+ <img src="{{ STATIC_URL }}img/no_image.gif" />
+ </div>
+ <div class="fileupload-preview fileupload-exists thumbnail" style="max-width: 150px; max-height: 150px; line-height: 20px;"></div>
+ <div>
+ <b>Peso</b>:
+ <input
+ class="input-small"
+ type="text"
+ name="groups.[[ group_order ]].fields.[[ field_order ]].options.[[ id ]].weight"
+ value="[[ weight ]]"
+ placeholder="peso" />
+ </center>
+ <span class="btn btn-file">
+ <span class="fileupload-new">Elegir</span>
+ <span class="fileupload-exists">Cambiar</span>
+ <input type="file" name="groups.[[ group_order ]].fields.[[ field_order ]].options.[[ id ]].img" />
+ </span>
+ <a href="#" class="btn fileupload-exists" data-dismiss="fileupload">Quitar</a>
+ </div>
+ </div>
+
+</script> \ No newline at end of file
diff --git a/webapp/polls/templates/poll-form.html b/webapp/polls/templates/poll-form.html
new file mode 100644
index 0000000..a182e5a
--- /dev/null
+++ b/webapp/polls/templates/poll-form.html
@@ -0,0 +1,81 @@
+{% extends "base-poll.html" %}
+{% load i18n %}
+
+{% block title %}Formulario de encuesta{% endblock %}
+
+{% block main_container %}
+
+ <div class="center">
+ <h2>{% trans 'Formulario de encuesta' %}</h2>
+ </div>
+
+ <form id="poll_form" class="form-inline" action="" method="post">{% csrf_token %}
+
+ {% if poll.id %}
+ <input type="hidden" name="id" value="{{ poll.id }}" />
+ {% endif %}
+
+ {% if user.is_superuser and poll.id %}
+ <div class="control-group {% if form.status.errors %}error{% endif %}">
+ <div class="controls">
+ <label class="control-label" for="id_status">{{ form.status.label }}:</label>
+ <select id="id_status" name="status">
+ {% for value, status in STATUS_CHOICES %}
+ <option value="{{ value }}" {% if status == poll.status %}selected="selected"{% endif %}>{{ status }}</option>
+ {% endfor %}
+ </select>
+ <span class="help-block">
+ <p>Una encuesta "cerrada" no podr&aacute; ser modificada. Una vez "cerrada", solo pordr&aacute; ser "abierta" por un usuario de mas alto privilegio.
+ </p>
+ </span>
+ {% if form.status.errors %}
+ <span class="help-inline">{{ form.status.errors }}</span>
+ {% endif %}
+ </div>
+ </div>
+ {% endif %}
+
+ <div class="control-group {% if form.name.errors %}error{% endif %}">
+ <div class="controls">
+ <label class="control-label" for="id_name">{{ form.name.label }}:</label>
+ <input type="text" name="name" id="id_name" placeholder="{{ form.name.label }}" value="{{ poll.name|default_if_none:'' }}" />
+ <span class="help-inline">{{ form.name.errors }}</span>
+ </div>
+ </div>
+
+ <div class="ps-form-toolbar btn-toolbar clearfix">
+ <div class="btn-group">
+ <!-- Save -->
+ <button class="btn btn-primary"><i class="icon-white icon-edit"></i>&nbsp;{% trans 'Guardar' %}</button>
+
+ <!-- Save and continue with poll structure -->
+ <button id="continue" class="btn btn-success"><i class="icon-white icon-circle-arrow-right"></i>&nbsp;{% trans 'Guardar y modificar estructura de encuesta' %}</button>
+ </div>
+ </div>
+ </form>
+
+
+<script type="text/javascript">
+
+ (function($){
+
+ var url = {
+ form_submit: '{% if poll.id %}{% url polls:edit poll.id %}{% else %}{% url polls:add %}{% endif %}?continue=true',
+ };
+
+ var form;
+
+ $(document).ready(function(){
+
+ form = $('#poll_form');
+
+ $('#continue').click(function(){
+ form.attr('action', url.form_submit);
+ form.trigger('submit');
+ });
+ });
+ })(jQuery);
+
+</script>
+
+{% endblock %} \ No newline at end of file
diff --git a/webapp/polls/templates/poll-list.html b/webapp/polls/templates/poll-list.html
new file mode 100644
index 0000000..bfa0eb3
--- /dev/null
+++ b/webapp/polls/templates/poll-list.html
@@ -0,0 +1,50 @@
+{% extends "base-poll.html" %}
+
+{% block main_container %}
+
+ <table class="table table-hover table-bordered">
+ <thead>
+ <tr>
+ <th class="span5">Nombre</th>
+ <th>Estado</th>
+ <th colspan="3"><center>Acciones</center></th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for poll in polls %}
+ <tr style="background-color: {% if poll.is_open %}#f2dede;{% else %}#e8f4db{% endif %}">
+ <td>{{ poll.name|capfirst }}</td>
+ <td>{{ poll.status|capfirst }}</td>
+ <td>
+ <a class="btn {% if not poll.is_open and not user.is_superuser %}disabled{% endif %}" href="{% url polls:edit id=poll.id %}">
+ <i class="icon-edit"></i>&nbsp;Modificar datos de encuesta
+ </a>
+ </td>
+ <td>
+ <a class="btn {% if not poll.is_open %}disabled{% endif %}" href="{% url polls:structure.builder poll_id=poll.id %}">
+ <i class="icon-wrench"></i>&nbsp;Modificar estructura
+ </a>
+ </td>
+ <td>
+ <a class="btn {% if poll.is_open %}disabled{% endif %}" href="{% url polls:download poll_id=poll.id %}">
+ <i class="icon-download-alt"></i>&nbsp;Descargar encuesta
+ </a>
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+
+<script type="text/javascript">
+
+ (function($){
+
+ $(document).ready(function(){
+ $('a.disabled').click(function() { return false; });
+ });
+
+ })(jQuery);
+
+</script>
+
+{% endblock %} \ No newline at end of file
diff --git a/webapp/polls/templates/poll-structure-form.html b/webapp/polls/templates/poll-structure-form.html
new file mode 100644
index 0000000..b695395
--- /dev/null
+++ b/webapp/polls/templates/poll-structure-form.html
@@ -0,0 +1,62 @@
+{% extends "base-poll.html" %}
+{% load i18n poll_tags %}
+
+{% block title %}Estructura de encuesta{% endblock %}
+
+{% block extra_css %}
+ <style type="text/css">
+ body { padding-top: 40px; }
+ </style>
+
+ <link href="{{ STATIC_URL }}css/bootstrap-fileupload.css" rel="stylesheet" />
+{% endblock %}
+
+{% block main_container %}
+ <script src="{{ STATIC_URL }}js/bootstrap-fileupload.js"></script>
+
+ <div class="alert alert-info">
+ <ul>
+ <li>
+ Para generar dependencias, arrastre el ID de la opci&oacute;n hacia el campo "Depende de la opci&oacute;n", o bien, escriba el ID indicado. <i class="icon-warning-sign"></i>&nbsp;En esta version no existen criterios sobre las dependecias, tenga cuidado de realizarlas correctamente y no generar inconsistencias (ej: no haga dependiente una pregunta de una opci&oacute;n perteneciente a la misma pregunta.)
+ </li>
+ <li>
+ La opciones con imagenes deber&aacute;n ser de dimensiones menores a 250px de ancho y 250px de alto. Formatos validos: GIF, JPG, PNG.
+ </li>
+ </ul>
+ </div>
+
+ <center><h2>{{ poll.name|capfirst }}</h2></center>
+
+ {% if errors %}
+ <div class="control-group error">
+ <label class="control-label">
+ <ul>
+ {% for error in errors %}
+ <li>{{ error }}</li>
+ {% endfor %}
+ </ul>
+ </label>
+ </div>
+ {% endif %}
+
+ <form class="form-inline" method="post" action="" enctype="multipart/form-data">
+
+ <input type="hidden" name="poll_id" value="{{ poll.id }}" >
+
+ {% if structure.id %}
+ <input type="hidden" name="id" value="{{ structure.id }}" />
+ {% endif %}
+
+ {% render_structure structure %}
+
+ <div class="ps-form-toolbar btn-toolbar clearfix">
+ <div class="btn-group">
+ <button class="btn btn-primary"><i class="icon-white icon-edit"></i>&nbsp;{% trans 'Guardar' %}</button>
+ </div>
+ </div>
+
+ {% csrf_token %}
+
+ </form>
+
+{% endblock %} \ No newline at end of file
diff --git a/webapp/polls/templates/poll-success.html b/webapp/polls/templates/poll-success.html
new file mode 100644
index 0000000..5d92d17
--- /dev/null
+++ b/webapp/polls/templates/poll-success.html
@@ -0,0 +1,19 @@
+{% extends "base-poll.html" %}
+
+{% block title %}Exito!{% endblock %}
+
+{% block main_container %}
+
+ <div class="row-fuild span12">
+ <form class="form-inline" method="post" action="">{% csrf_token %}
+
+ <div class="ps-form-toolbar btn-toolbar clearfix">
+ <div class="btn-group">
+ <button class="btn btn-primary"><i class="icon-white icon-download-alt"></i>&nbsp;Descargar encuesta</button>
+ </div>
+ </div>
+
+ </form>
+ </div>
+
+{% endblock %} \ No newline at end of file
diff --git a/webapp/polls/templates/sucess.html b/webapp/polls/templates/sucess.html
deleted file mode 100644
index 364b31b..0000000
--- a/webapp/polls/templates/sucess.html
+++ /dev/null
@@ -1,28 +0,0 @@
-{% extends "base-main.html" %}
-
-{% block title %}Success!{% endblock %}
-
-{% block main_container %}
-
- <div class="row-fuild alert alert-success span10">
- La estructura de la encuesta se generó con exito, puede descargarla.
- </div>
-
- <div class="row-fuild span12">
- <form class="form-inline" method="post" action="">
-
- <div class="ps-form-toolbar btn-toolbar clearfix">
- <div class="btn-group">
- <button class="btn btn-primary"><i class="icon-white icon-download-alt"></i>&nbsp;Descargar encuesta</button>
- </div>
- <div class="btn-group">
- <a class="btn btn-warning " href="{% url polls:builder %}"><i class="icon-white icon-wrench"></i>&nbsp;Volver al constructor de estructuras</a>
- </div>
- </div>
-
- {% csrf_token %}
-
- </form>
- </div>
-
-{% endblock %} \ No newline at end of file
diff --git a/webapp/polls/templates/tags/field-options.html b/webapp/polls/templates/tags/field-options.html
deleted file mode 100644
index 6f352db..0000000
--- a/webapp/polls/templates/tags/field-options.html
+++ /dev/null
@@ -1,243 +0,0 @@
-{% load i18n %}
-
-<!-- +++++++++++++++++++++++++++++++ -->
-<!-- ++ Widget: Field Options ++ -->
-<!-- +++++++++++++++++++++++++++++++ -->
-
-<div id="WAddOptionButton_container_{{ group_index }}_{{ field_index }}"></div>
-
-<div id="WFieldOptions_{{ group_index }}_{{ field_index }}">
- <div id="WFieldOptions_container_{{ group_index }}_{{ field_index }}"></div>
-</div>
-
-<!-- Template: Field Option -->
-<script type="text/x-mustache-template" name="field_option">
- <div class="span6 input-append">
- <label>
- Opci&oacute;n (ID: <span _group_index="[[ group_index ]]"
- _field_index="[[ field_index ]]" class="draggable"><i class="icon-move"></i>[[ id ]]</span>)</label><br />
- <input
- class="input-large"
- type="text"
- name="groups.[[ group_index ]].fields.[[ field_index ]].options.[[ id ]].text"
- value="[[ value ]]"
- placeholder="opci&oacute;n" />
- <button class="WFieldOptions_remove btn btn-danger">
- <i class="icon-remove-sign icon-white"></i>
- </button>
- </div>
-</script>
-
-<!-- Template: Add Option button -->
-<script type="text/x-mustache-template" name="add_button">
- <div class="control-group">
- <div class="controls">
- <button id="WFieldOptions_add_[[ group_index ]]_[[ field_index ]]" class="btn btn-success">
- <i class="icon-plus-sign icon-white"></i> {% trans "Agregar opci&oacute;n" %}
- </button>
- </div>
- </div>
-</script>
-
-<!-- Template: Field Option default (widget type: RadioButton) -->
-<script type="text/x-mustache-template" name="field_option_default">
- <label>Texto default:</label>
- <input
- class="input-xlarge"
- type="text"
- name="groups.[[ group_index ]].fields.[[ field_index ]].options.[[ id ]].text"
- value="[[ value ]]"
- placeholder="(opcional)" />
-</script>
-
-<script type="text/javascript">
- /***************************/
- /** Widget: Field Options **/
- /***************************/
-
- (function($){
-
- var options = {{ options }},
- offset_option_id = {{ offset_option_id }},
- default_value = '',
- field_widget_type = $('#WFieldWidgetType_{{ group_index }}_{{ field_index }}'),
- group_index = {{ group_index }},
- field_index = {{ field_index }},
- field_options_count = parseInt({% if field.options|length %}{{ field.options|length }}{% else %}0{% endif %}),
- max_field_options = 4,
- widget,
- container,
- button_container;
-
- TEMPLATES = {};
-
- var factoryOptionDefault = function(id, value) {
-
- var option_default = $(
- Mustache.render(TEMPLATES['field_option_default'], {
- "id": id,
- "value": value,
- "group_index": group_index,
- "field_index": field_index
- })
- ),
- row_fluid = $('<div class="row-fluid"></div>');
-
- row_fluid.append(option_default);
- container.append(row_fluid);
- }
-
- var factoryOption = function(id, value) {
- // TODO: Desacoplar los widgets de este lugar.
- var with_options = [ "MultipleCheckBox", "DropDownList", "RadioButton" ],
- type = field_widget_type.attr('value');
-
- if ($.inArray(type, with_options) != -1){
- var option = $(
- Mustache.render(TEMPLATES['field_option'], {
- "id": id,
- "value": value,
- "group_index": group_index,
- "field_index": field_index
- })
- );
-
- option.find('button').on('click', function(event) {
- event.preventDefault();
- var container_row_fuild = option.parent(".row-fluid");
-
- option.remove();
-
- /* Check for remove row of options */
- if (container_row_fuild.contents().length == 0) {
- console.log(container_row_fuild);
- container_row_fuild.remove();
- }
-
- /* Allow add more options */
- field_options_count = $("#WFieldOptions_container_" + group_index + "_" + field_index + " [type='text']").length;
- var add_button = $("#WFieldOptions_add_" + group_index + "_" + field_index);
- if (field_options_count <= max_field_options){
- add_button.removeClass("disabled");
- add_button.removeAttr("disabled");
- }
- });
-
- row_fluid = $('<div class="row-fluid"></div>');
- row_fluid.append(option);
- container.append(row_fluid);
-
- } else {
- factoryOptionDefault(id, value);
- }
-
- /** Draggable option ID to dependence field **/
- $( ".draggable" ).draggable({'helper': 'clone'});
- $( ".droppable" ).droppable({
- drop: function( event, ui ) {
- var drag_group_index = parseInt($(ui.draggable).attr('_group_index')),
- drag_field_index = parseInt($(ui.draggable).attr('_field_index')),
- drop_group_index = parseInt($(this).attr('_group_index')),
- drop_field_index = parseInt($(this).attr('_field_index'));
-
- // TODO: Hacer comprobacion del lado del server tmb.
- if (drag_group_index == drop_group_index && drag_field_index < drop_field_index) {
- var value = ui.draggable[0]['innerText'];
- $(this)
- .addClass( "ui-state-highlight" )
- .attr("value", value);
- } else {
- alert('No se puede hacer una dependencia hacia atras.');
- }
- }
- });
- $( '.droppable[type="text"]' ).focusout(function(){
- if ($(this).attr('value') == ""){
- $(this).removeClass( "ui-state-highlight" );
- }
- });
-
- };
-
- var bind_add_button = function(add_button) {
- add_button.on('click', function(event){
- event.preventDefault();
-
- factoryOption(offset_option_id, default_value);
- offset_option_id++;
-
- /* Deny add more options */
- field_options_count++;
- if (field_options_count >= max_field_options){
- add_button.addClass("disabled");
- add_button.attr("disabled", "disabled");
- }
- });
- }
-
- var factoryAddButton = function() {
- /** render and get add button **/
- var add_button = $(
- Mustache.render(TEMPLATES['add_button'], {
- "group_index": group_index,
- "field_index": field_index
- })
- ).find('button');
-
- /** Bind change event for field widget type select box **/
- // TODO: Desacoplar los widgets de este lugar.
- field_widget_type.on('change', function(event) {
- field_options_count = 0;
- var with_options = [ "MultipleCheckBox", "DropDownList", "RadioButton" ];
- var type = $(this).attr('value');
- container.contents().remove()
- if ($.inArray(type, with_options) == -1) {
- offset_option_id++;
- add_button.remove();
- factoryOptionDefault(offset_option_id, '');
- } else {
- bind_add_button(add_button)
- button_container.append(add_button);
- }
- /* Allow add more options */
- if (field_options_count < max_field_options){
- add_button.removeClass("disabled");
- add_button.removeAttr("disabled");
- }
- });
-
- if ("{{ field.need_options }}" == "True"){
- button_container.append(add_button);
- bind_add_button(add_button)
- }
-
- };
-
- $(document).ready(function() {
-
- /** Preparing TEMPLATES **/
- $('script[type="text/x-mustache-template"]').each(function(i, obj){
- TEMPLATES[$(obj).attr('name')] = $(obj).text();
- });
-
- /** get the widget **/
- widget = $('#WFieldOptions_{{ group_index }}_{{ field_index }}');
- container = widget.find('#WFieldOptions_container_{{ group_index }}_{{ field_index }}');
-
- /** Populate the current options **/
- $.each(options, factoryOption);
-
- /** get the container for buttons **/
- button_container = $('#WAddOptionButton_container_{{ group_index }}_{{ field_index }}');
-
- /** Prepare add_button **/
- factoryAddButton();
-
- });
-
- })(jQuery);
-</script>
-
-<!-- +++++++++++++++++++++++++++++++++++ -->
-<!-- ++ END Widget: Field Options ++ -->
-<!-- +++++++++++++++++++++++++++++++++++ --> \ No newline at end of file
diff --git a/webapp/polls/templates/tags/field-widget-types.html b/webapp/polls/templates/tags/field-widget-types.html
deleted file mode 100644
index f8768c3..0000000
--- a/webapp/polls/templates/tags/field-widget-types.html
+++ /dev/null
@@ -1,6 +0,0 @@
-<label>Tipo de pregunta:</label>
-<select id="WFieldWidgetType_{{ group_index }}_{{ field_index }}" name="groups.{{ group_index }}.fields.{{ field_index }}.widget_type">
- {% for k, v in WIDGET_TYPES %}
- <option value="{{ k }}" {% if k == field.widget_type %}selected="selected"{% endif %}>{{ v }}</option>
- {% endfor %}
-</select> \ No newline at end of file
diff --git a/webapp/polls/templates/tags/structure.html b/webapp/polls/templates/tags/structure.html
new file mode 100644
index 0000000..9b525b8
--- /dev/null
+++ b/webapp/polls/templates/tags/structure.html
@@ -0,0 +1,71 @@
+{% load i18n poll_tags %}
+
+<!-- Containers -->
+<button id="WGroup_add" class="btn btn-primary">
+ <i class="icon-plus-sign icon-white"></i>&nbsp;{% trans 'Agregar grupo' %}
+</button>
+
+<div id="WGroupContainer"></div>
+
+
+<!-- Mustache templates -->
+{% include "mustache/group.html" %}
+
+{% include "mustache/field.html" %}
+
+{% include "mustache/option.html" %}
+
+{% include "mustache/option_default.html" %}
+
+{% include "mustache/option_image_upload.html" %}
+
+{% include "mustache/option_image_thumbnail.html" %}
+
+<!-- Global variables for dynamic_structure.js -->
+<script type="text/javascript">
+
+ var groups = {{ groups|json }},
+ WIDGET_TYPES = {{ WIDGET_TYPES|json }},
+ WITH_OPTIONS = {{ WITH_OPTIONS|json }},
+ WITH_IMAGES = {{ WITH_IMAGES|json }},
+ OFFSET_OPTION_ID = {{ OFFSET_OPTION_ID|json }},
+ IMAGE_OPTIONS_TMP_MEDIA_URL = {{ IMAGE_OPTIONS_TMP_MEDIA_URL|json }},
+ POLL_ID = {{ POLL_ID|json }};
+
+</script>
+
+
+<!-- Dynamic structure methods -->
+<script src="{{ STATIC_URL }}js/dynamic_structure.js"></script>
+
+
+<!-- Dynamic structure initialization -->
+<script type="text/javascript">
+ (function($){
+
+ $(document).ready(function() {
+
+ // Preparing TEMPLATES
+ $('script[type="text/x-mustache-template"]').each(function(i, obj){
+ TEMPLATES[$(obj).attr('name')] = $(obj).text();
+ });
+
+ // Get group container widget
+ container = $('#WGroupContainer');
+
+ // Render of groups
+ $.each(groups, factoryGroup);
+
+ // Bind group add event
+ $("#WGroup_add").on('click', function(event){
+ event.preventDefault();
+
+ // TODO: Que la estructura de grupo vacio la de un metodo.
+ var next_group_order = $('.group').length;
+ factoryGroup(next_group_order, {"name": '', "fields": []})
+ });
+
+ });
+
+ })(jQuery);
+</script> \ No newline at end of file
diff --git a/webapp/polls/templatetags/poll_tags.py b/webapp/polls/templatetags/poll_tags.py
index 8c4a358..9a8c93a 100644
--- a/webapp/polls/templatetags/poll_tags.py
+++ b/webapp/polls/templatetags/poll_tags.py
@@ -4,44 +4,32 @@ from django import template
from django.utils.safestring import SafeUnicode
from django.template.loader import render_to_string
+from polls.models import WIDGET_TYPES, WITH_OPTIONS, WITH_IMAGES, Field
-register = template.Library()
+register = template.Library()
-@register.simple_tag(takes_context=True)
-def render_options(context, field):
- #TODO: Solucionar esto de alguna otra forma
- options = field.options if field.options is not None else []
- options = dict(((opt.id, opt.text) for opt in options))
+@register.filter(name="json")
+def json_filter(obj):
+ return SafeUnicode(json.dumps(obj))
- #TODO: Esto, es un problema, por el hecho que los campos default
- # y options son la misma cosa, ELIMINAR ESTA AMBIGUEDAD
- if not len(options) and not field.need_options():
- options = dict(((field.get_offset_id(), ''), ))
- options = SafeUnicode(json.dumps(options))
+@register.simple_tag(takes_context=True)
+def render_structure(context, structure):
+ structure_data = structure.to_python(with_errors=True)
+ IMAGE_OPTIONS_TMP_MEDIA_URL = structure.get_image_options_tmp_media_url()
return render_to_string(
- 'tags/field-options.html',
+ 'tags/structure.html',
{
- "offset_option_id": SafeUnicode(json.dumps(field.get_offset_id())),
- "options": options,
- "field": field,
- #TODO: Solucionar esto de alguna otra forma
- "group_index": context['group_index'],
- "field_index": context['forloop']['counter0']
+ "STATIC_URL": context['STATIC_URL'],
+ "WIDGET_TYPES": [{'key': k, 'value': v} for k, v in WIDGET_TYPES],
+ "WITH_OPTIONS": WITH_OPTIONS,
+ "WITH_IMAGES": WITH_IMAGES,
+ "OFFSET_OPTION_ID": Field.get_offset_id(),
+ "IMAGE_OPTIONS_TMP_MEDIA_URL": IMAGE_OPTIONS_TMP_MEDIA_URL,
+ "POLL_ID": str(context['poll'].id),
+ "groups": structure_data.get("groups", {})
}
)
-
-
-@register.inclusion_tag('tags/field-widget-types.html', takes_context=True)
-def render_widget_types(context, field):
- return {
- "need_options": field.need_options(),
- "field": field,
- "WIDGET_TYPES": context['WIDGET_TYPES'],
- #TODO: Solucionar esto de alguna otra forma
- "group_index": context['group_index'],
- "field_index": context['forloop']['counter0']
- }
diff --git a/webapp/polls/tests.py b/webapp/polls/tests.py
deleted file mode 100644
index ed15c18..0000000
--- a/webapp/polls/tests.py
+++ /dev/null
@@ -1,214 +0,0 @@
-# -*- encoding: utf-8 -*-
-from django.test import TestCase
-
-from polls.models import Field, Group, Structure, Option, WIDGET_TYPES
-
-
-class FieldsTests(TestCase):
-
- def setUp(self):
- self.field = Field(
- key='0001',
- name='field_0',
- widget_type='TextInput',
- )
-
- def test_basic_field(self):
-
- self.assertEqual('0001', self.field.key)
- self.assertEqual('field_0', self.field.name)
- self.assertEqual('TextInput', self.field.widget_type)
-
- def test_invalid_widget_type(self):
- field_factory = lambda: Field(
- key="0001",
- name='field_0',
- widget_type='bad_widget_type',
- )
-
- self.assertRaises(AttributeError, field_factory)
-
- def test_valid_widget_types(self):
- for widget_type in dict(WIDGET_TYPES).keys():
- field = Field(
- key='0001',
- name='field_0',
- widget_type=widget_type,
- )
- self.assertTrue(field)
-
- def test_add_dependence(self):
- dependence = '414541212'
- self.field.add_dependence(dependence)
-
- self.assertEqual(dependence, self.field.dependence)
-
- def test_from_dict(self):
- dependence = '343523523'
- data = {
- 'widget_type': 'MultipleCheckBox',
- 'name': 'field_0',
- 'dependence': dependence
- }
- field = Field.from_dict(data)
-
- self.assertEqual('field_0', field.name)
- self.assertEqual('MultipleCheckBox', field.widget_type)
- self.assertEqual(dependence, field.dependence)
-
- def test_add_options(self):
- data = {
- 'widget_type': 'MultipleCheckBox',
- 'name': 'field_0',
- }
- field = Field.from_dict(data)
-
- options_data = {
- '1': {'text': None},
- '2': {'text': None},
- '3': {'text': 'test', 'enable': '0001'}
- }
- field.add_options(options_data)
-
- self.assertEqual(1, len(field.options))
-
- def test_validate(self):
-
- self.field.widget_type = "MultipleCheckBox"
-
- self.assertRaises(Field.ValidationError, self.field.validate)
- self.assertEqual(1, len(self.field.errors))
-
- self.field.widget_type = "RadioButton"
-
- self.assertRaises(Field.ValidationError, self.field.validate)
- self.assertEqual(1, len(self.field.errors))
-
- self.field.widget_type = "DropDownList"
-
- self.assertRaises(Field.ValidationError, self.field.validate)
- self.assertEqual(1, len(self.field.errors))
-
-
-class OptionTests(TestCase):
-
- def test_basic_option(self):
- option = Option(id='id', text='text')
-
- self.assertEqual('text', option.text)
- self.assertIsNotNone(option.id)
-
- def test_from_dict(self):
- data = {'id': '1', 'text': 'si'}
- option = Option.from_dict(data)
-
- self.assertEqual('1', option.id)
- self.assertEqual('si', option.text)
-
-
-class GroupTests(TestCase):
-
- def setUp(self):
- self.group = Group(name='group_name')
-
- def test_basic_group(self):
- group = Group(name='group_name')
-
- self.assertEqual('group_name', group.name)
- self.assertEqual([], group.fields)
-
- def test_add_field(self):
- len_group = len(self.group.fields)
-
- field_0 = Field(key='0001', name='field_name', widget_type='TextInput')
- self.group.add_field(field_0, 0)
-
- self.assertEqual(len_group + 1, len(self.group.fields))
- self.assertIsInstance(field_0, Field)
-
- field_1 = Field(key='0002', name='field_name', widget_type='TextInput')
- self.group.add_field(field_1, 0)
-
- self.assertEqual(field_0, self.group.fields[1])
- self.assertEqual(field_1, self.group.fields[0])
-
-
-class StructureTests(TestCase):
-
- def test_from_dict(self):
- data = {
- 'groups': {
- '0': {
- 'name': 'group_0',
- 'fields': {
- '0': {
- 'widget_type': 'TextInput',
- 'name': 'field_0_0'
- },
- '1': {
- 'widget_type': 'RadioButton',
- 'name': 'field_0_1'
- }
- },
- },
- '1': {
- 'name': 'group_1',
- 'fields': {
- '0': {
- 'widget_type': 'MultipleCheckBox',
- 'name': 'field_1_0'
- },
- '1': {
- 'widget_type': 'DropDownList',
- 'name': 'field_1_1'
- }
- },
- }
- }
- }
-
- structure = Structure(data=data)
-
- group_0 = structure.groups[0]
- self.assertEqual('group_0', group_0.name)
- self.assertEqual(2, len(group_0.fields))
-
- group_1 = structure.groups[1]
- self.assertEqual('group_1', group_1.name)
- self.assertEqual(2, len(group_1.fields))
-
- field_0_0 = group_0.fields[0]
- self.assertEqual('field_0_0', field_0_0.name)
- self.assertEqual('TextInput', field_0_0.widget_type)
- field_0_1 = group_0.fields[1]
- self.assertEqual('field_0_1', field_0_1.name)
- self.assertEqual('RadioButton', field_0_1.widget_type)
- field_1_0 = group_1.fields[0]
- self.assertEqual('field_1_0', field_1_0.name)
- self.assertEqual('MultipleCheckBox', field_1_0.widget_type)
- field_1_1 = group_1.fields[1]
- self.assertEqual('field_1_1', field_1_1.name)
- self.assertEqual('DropDownList', field_1_1.widget_type)
-
- def test_is_valid(self):
- data = {
- 'groups': {
- '0': {
- 'name': 'group_0',
- 'fields': {
- '0': {
- 'widget_type': 'RadioButton',
- 'name': 'field_0_0'
- },
- }
- }
- }
- }
-
- structure = Structure(data=data)
-
- self.assertFalse(structure.is_valid())
-
- field_0_0 = structure.groups[0].fields[0]
-
- self.assertEqual(1, len(field_0_0.errors))
diff --git a/webapp/webapp/media/output/empty b/webapp/polls/tests/__init__.py
index e69de29..e69de29 100644
--- a/webapp/webapp/media/output/empty
+++ b/webapp/polls/tests/__init__.py
diff --git a/webapp/polls/tests/field_tests.py b/webapp/polls/tests/field_tests.py
new file mode 100644
index 0000000..04e7cd9
--- /dev/null
+++ b/webapp/polls/tests/field_tests.py
@@ -0,0 +1,211 @@
+# -*- encoding: utf-8 -*-
+from polls.models import Poll, Field, WIDGET_TYPES, WITH_OPTIONS, WITH_IMAGES
+
+from utils.test import MongoTestCase, mock_in_memory_image
+
+
+class FieldTests(MongoTestCase):
+
+ def setUp(self):
+ poll = Poll(data={'name': 'name'})
+ poll_id = poll.save()
+
+ self.poll = Poll.get(id=poll_id)
+
+ data = {
+ 'name': "field_0",
+ 'widget_type': Field.TextInput,
+ 'dependence': "414541212",
+ 'order': 0
+ }
+ self.field = Field(data, poll=self.poll)
+
+ def test_field(self):
+ self.assertEqual(0, self.field.order)
+ self.assertEqual('field_0', self.field.name)
+ self.assertEqual(Field.TextInput, self.field.widget_type)
+ self.assertEqual("414541212", self.field.dependence)
+
+ def test_invalid_widget_type(self):
+ field_factory = lambda: Field({
+ 'name': "field_0",
+ 'widget_type': "bad_widget_type",
+ })
+
+ self.assertRaises(AttributeError, field_factory)
+
+ def test_valid_widget_types(self):
+ for widget_type in dict(WIDGET_TYPES).keys():
+
+ field = Field({
+ 'name': 'field_0',
+ 'widget_type': widget_type,
+ })
+ self.assertTrue(field)
+
+ def test_add_options(self):
+ self.field.widget_type = Field.MultipleCheckBox
+
+ options_data = {
+ '1': {'text': None},
+ '2': {'text': None},
+ '3': {'text': 'test', 'enable': '0001'}
+ }
+ self.field.add_options(options_data)
+
+ self.assertEqual(1, len(self.field.options))
+
+ option = self.field.options[0]
+ self.assertEqual(self.poll, option.poll)
+
+ def test_add_image_options(self):
+
+ self.field.widget_type = Field.ImageCheckBox,
+
+ img_file = mock_in_memory_image()
+
+ options_data = {
+ '1': {'img': img_file},
+ }
+ self.field.add_options(options_data)
+
+ self.assertEqual(1, len(self.field.options))
+
+ def test_validate_without_options(self):
+
+ self.field.widget_type = Field.MultipleCheckBox
+
+ self.assertRaises(Field.ValidationError, self.field.validate)
+ self.assertEqual(1, len(self.field.errors))
+
+ self.field.widget_type = Field.RadioButton
+
+ self.assertRaises(Field.ValidationError, self.field.validate)
+ self.assertEqual(1, len(self.field.errors))
+
+ self.field.widget_type = Field.DropDownList
+
+ self.assertRaises(Field.ValidationError, self.field.validate)
+ self.assertEqual(1, len(self.field.errors))
+
+ self.field.widget_type = Field.ImageCheckBox
+
+ self.assertRaises(Field.ValidationError, self.field.validate)
+ self.assertEqual(1, len(self.field.errors))
+
+ self.field.widget_type = Field.ImageRadioButton
+
+ self.assertRaises(Field.ValidationError, self.field.validate)
+ self.assertEqual(1, len(self.field.errors))
+
+ def test_to_python(self):
+
+ # Widget_type = TextInput
+ self.field.widget_type = Field.TextInput
+ self.field.add_options({'1': {'text': "text"}})
+
+ expected = {
+ '0': {
+ 'name': "field_0",
+ 'widget_type': Field.TextInput,
+ 'dependence': "414541212",
+ 'options': {
+ '1': {
+ 'text': "text"
+ }
+ }
+ }
+ }
+
+ self.assertEqual(expected, self.field.to_python())
+
+ # Widget_type = TextInput
+ self.field.widget_type = Field.TextInput
+ self.field.dependence = ""
+ self.field.add_options({'1': {'text': "text"}})
+
+ expected = {
+ '0': {
+ 'name': "field_0",
+ 'widget_type': Field.TextInput,
+ 'options': {
+ '1': {
+ 'text': "text"
+ }
+ }
+ }
+ }
+
+ self.assertEqual(expected, self.field.to_python())
+
+ # Widget_type kind with options
+ order = 0
+ for widget_type in (set(WITH_OPTIONS) - set(WITH_IMAGES)):
+ data = {
+ 'name': "field_0",
+ 'widget_type': widget_type,
+ 'dependence': "414541212",
+ 'order': order
+ }
+ field = Field(data)
+ field.add_options({
+ '1': {'text': "text", 'weight': 100},
+ '2': {'text': "text", 'weight': -100}
+ })
+
+ expected = {
+ '%d' % order: {
+ 'name': "field_0",
+ 'widget_type': widget_type,
+ 'dependence': "414541212",
+ 'options': {
+ '1': {
+ 'text': "text",
+ 'weight': 100
+ },
+ '2': {
+ 'text': "text",
+ 'weight': -100
+ }
+ }
+ }
+ }
+
+ self.assertEqual(expected, field.to_python())
+ order += 1
+
+ # Widget_type kind with options with images
+ order = 0
+ for widget_type in WITH_IMAGES:
+ data = {
+ 'name': "field_0",
+ 'widget_type': widget_type,
+ 'dependence': "414541212",
+ 'order': order
+ }
+ field = Field(data)
+ field.add_options({
+ '1': {'img_name': "text.jpg", 'weight': 100},
+ '2': {'img_name': "text.jpg", 'weight': -100}
+ })
+
+ expected = {
+ '%d' % order: {
+ 'name': "field_0",
+ 'widget_type': widget_type,
+ 'dependence': "414541212",
+ 'options': {
+ '1': {
+ 'img_name': "text.jpg",
+ 'weight': 100
+ },
+ '2': {
+ 'img_name': "text.jpg",
+ 'weight': -100
+ }
+ }
+ }
+ }
+
+ self.assertEqual(expected, field.to_python())
+ order += 1
diff --git a/webapp/polls/tests/group_tests.py b/webapp/polls/tests/group_tests.py
new file mode 100644
index 0000000..794136e
--- /dev/null
+++ b/webapp/polls/tests/group_tests.py
@@ -0,0 +1,79 @@
+# -*- encoding: utf-8 -*-
+from django.test import TestCase
+
+from polls.models import Group, Field
+
+
+class GroupTests(TestCase):
+
+ def setUp(self):
+ data = {'name': "group name", 'order': 0}
+ self.group = Group(data)
+
+ def test_group(self):
+ self.assertEqual('group name', self.group.name)
+ self.assertEqual(0, self.group.order)
+ self.assertEqual([], self.group.fields)
+
+ def test_add_field(self):
+ len_group = len(self.group.fields)
+
+ field_0 = Field({'name': 'field_name', 'widget_type': Field.TextInput})
+ self.group.add_field(field_0, 0)
+
+ self.assertEqual(len_group + 1, len(self.group.fields))
+ self.assertEqual(0, field_0.order)
+ self.assertIsInstance(field_0, Field)
+
+ field_1 = Field({'name': 'field_name', 'widget_type': Field.TextInput})
+ self.group.add_field(field_1, 1)
+ self.assertEqual(1, field_1.order)
+
+ self.assertEqual(field_0, self.group.fields[0])
+ self.assertEqual(field_1, self.group.fields[1])
+
+ def test_validate(self):
+
+ group = Group({'name': None, 'fields': None})
+
+ self.assertRaises(Group.ValidationError, group.validate)
+ self.assertEqual(2, len(group.errors))
+
+ group = Group({'name': "", 'fields': None})
+ self.assertRaises(Group.ValidationError, group.validate)
+ self.assertEqual(2, len(group.errors))
+
+ group = Group({'name': "name", 'fields': None})
+ self.assertRaises(Group.ValidationError, group.validate)
+ self.assertEqual(1, len(group.errors))
+
+ def test_to_python(self):
+
+ expected = {
+ '0': {
+ 'name': "group name",
+ 'fields': {
+ '0': {
+ 'name': "field_0",
+ 'widget_type': Field.TextInput,
+ 'options': {
+ '131212': {
+ 'text': "text"
+ }
+ }
+ }
+ }
+ }
+ }
+
+ field = Field({
+ 'name': "field_0",
+ 'widget_type': Field.TextInput,
+ 'order': 0
+ })
+ field.add_options({'131212': {'text': "text"}})
+
+ group = Group({'name': "group name", 'order': 0})
+ group.add_field(field, field.order)
+
+ self.assertEqual(expected, group.to_python())
diff --git a/webapp/polls/tests/option_tests.py b/webapp/polls/tests/option_tests.py
new file mode 100644
index 0000000..cbfbbb1
--- /dev/null
+++ b/webapp/polls/tests/option_tests.py
@@ -0,0 +1,155 @@
+# -*- encoding: utf-8 -*-
+from django.conf import settings
+
+from polls.models import Poll, Option
+
+from utils.test import MongoTestCase, mock_in_memory_image, mock_text_file
+
+
+class OptionTests(MongoTestCase):
+
+ def setUp(self):
+ poll = Poll(data={'name': 'name'})
+ poll_id = poll.save()
+
+ self.poll = Poll.get(id=poll_id)
+
+ def test_option(self):
+
+ data = {'id': "1"}
+ option = Option(data)
+ self.assertEqual('1', option.id)
+
+ def test_option_text(self):
+
+ data = {'id': "1", 'text': "text"}
+ option = Option(data)
+ self.assertEqual('text', option.text)
+
+ def test_option_text_and_weight(self):
+
+ data = {'id': "1", 'text': "text", 'weight': "100"}
+ option = Option(data)
+ self.assertEqual('text', option.text)
+ self.assertEqual(100, option.weight)
+
+ def test_option_img(self):
+
+ img_file = mock_in_memory_image('image.jpg')
+ data = {'id': "1", 'img': img_file}
+ option = Option(data)
+ self.assertEqual(img_file, option.img)
+ self.assertEqual('%s.jpg' % option.id, option.img_name)
+
+ def test_option_img_and_weight(self):
+
+ img_file = mock_in_memory_image('image.jpg')
+ data = {'id': "1", 'img': img_file, 'weight': "100"}
+ option = Option(data)
+ self.assertEqual(img_file, option.img)
+ self.assertEqual('%s.jpg' % option.id, option.img_name)
+ self.assertEqual(100, option.weight)
+
+ def test_option_validation_weight_required(self):
+
+ data = {'id': '1'}
+ option = Option(data)
+
+ self.assertRaises(Option.ValidationError, option.validate)
+ msg = u"opcion %s: ponderación requerida." % option.id
+ self.assertTrue(msg in option.errors)
+
+ def test_image_option_get_absolute_path(self):
+
+ img_file = mock_in_memory_image(size=(250, 250))
+
+ data = {'id': '1', 'img': img_file}
+ option = Option(data, poll=self.poll)
+
+ expected_path = "%s/%s/%s" % (
+ settings.IMAGE_OPTIONS_ROOT, str(option.poll.id), option.img_name
+ )
+ self.assertEqual(expected_path, option.get_absolute_path())
+
+ def test_image_option_validation_invalid_image_file(self):
+ invalid_img_file = mock_text_file()
+
+ data = {'id': '1', 'img': invalid_img_file}
+ option = Option(data)
+
+ self.assertRaises(Option.ValidationError, option.validate)
+ self.assertIsNone(option.img)
+ self.assertIsNone(option.img_name)
+
+ def test_image_option_validation_invalid_size(self):
+
+ invalid_img_file = mock_in_memory_image(size=(300, 200))
+
+ data = {'id': '1', 'img': invalid_img_file}
+ option = Option(data)
+
+ self.assertRaises(Option.ValidationError, option.validate)
+ msg = u"%s: Se necesita una imagen menor a 250x250." % option.id
+ self.assertTrue(msg in option.errors)
+ self.assertIsNone(option.img)
+ self.assertIsNone(option.img_name)
+
+ invalid_img_file = mock_in_memory_image(size=(200, 300))
+
+ data = {'id': '1', 'img': invalid_img_file}
+ option = Option(data)
+
+ self.assertRaises(Option.ValidationError, option.validate)
+ msg = u"%s: Se necesita una imagen menor a 250x250." % option.id
+ self.assertTrue(msg in option.errors)
+ self.assertIsNone(option.img)
+ self.assertIsNone(option.img_name)
+
+ img_file = mock_in_memory_image(size=(250, 250))
+
+ data = {'id': '1', 'img': img_file}
+ option = Option(data)
+
+ self.assertTrue(len(option.errors) == 0)
+
+ def test_to_python(self):
+
+ img_option_expected = {
+ "1": {
+ "img_name": "1.jpg",
+ "weight": 100
+ }
+ }
+
+ option = Option()
+ option.id = "1"
+ option.img_name = "1.jpg"
+ option.weight = 100
+
+ self.assertEqual(img_option_expected, option.to_python())
+
+ basic_option_expected = {
+ "1": {
+ "text": "some text",
+ "weight": 100
+ }
+ }
+
+ option = Option()
+ option.id = "1"
+ option.text = "some text"
+ option.weight = 100
+
+ self.assertEqual(basic_option_expected, option.to_python())
+
+ default_option_expected = {
+ "1": {
+ "text": "some text",
+ }
+ }
+
+ option = Option()
+ option.id = "1"
+ option.text = "some text"
+
+ self.assertEqual(default_option_expected, option.to_python())
diff --git a/webapp/polls/tests/poll_tests.py b/webapp/polls/tests/poll_tests.py
new file mode 100644
index 0000000..2bf04ad
--- /dev/null
+++ b/webapp/polls/tests/poll_tests.py
@@ -0,0 +1,136 @@
+# -*- encoding: utf-8 -*-
+from bson import ObjectId
+
+from polls.models import Poll
+from polls.forms import PollAddForm
+
+from utils.test import MongoTestCase
+
+
+class PollTests(MongoTestCase):
+
+ def test_poll_init(self):
+ name = 'nombre de encuesta'
+ data = {'name': name}
+
+ poll = Poll(data=data)
+ self.assertIsNone(poll.id)
+ self.assertEqual(name, poll.name)
+
+ id = '513a0634421aa932884daa5c'
+ data = {'id': id, 'name': name}
+
+ poll = Poll(data=data)
+ self.assertEqual(ObjectId(id), poll.id)
+ self.assertEqual(name, poll.name)
+
+ def test_validate(self):
+ data = {}
+ poll = Poll(data=data)
+
+ self.assertRaises(Poll.ValidationError, poll.validate)
+
+ def test_save(self):
+
+ name = 'nombre de encuesta'
+ data = {'name': name}
+
+ poll = Poll(data=data)
+ poll.save()
+
+ self.assertEqual(1, self.db.polls.count())
+
+ def test_unique_name(self):
+
+ name = "unique name"
+ data = {'name': name}
+
+ poll = Poll(data=data)
+ poll.save()
+
+ poll = Poll(data=data)
+
+ self.assertRaisesRegexp(
+ Poll.ValidationError,
+ "Poll name '%s' already in use." % poll.name,
+ poll.validate
+ )
+
+ data = {'name': "name"}
+ poll = Poll(data=data)
+
+ fail = True
+ try:
+ poll.save()
+ except Poll.ValidationError:
+ fail = False
+
+ self.assertTrue(fail)
+
+ def test_get(self):
+
+ self.assertIsNone(Poll.get(id=None))
+
+ name = 'nombre de encuesta'
+ data = {'name': name}
+
+ poll = Poll(data=data)
+ poll_id = poll.save()
+
+ poll = Poll.get(id=poll_id)
+
+ self.assertIsNotNone(poll)
+ self.assertIsNotNone(poll.id)
+ self.assertIsInstance(poll_id, ObjectId)
+ self.assertEqual(name, poll.name)
+ self.assertEqual(Poll.OPEN, poll.status)
+
+ def test_status(self):
+
+ name = 'nombre de encuesta'
+ data = {'name': name}
+
+ poll = Poll(data=data)
+
+ self.assertEqual(Poll.OPEN, poll.status)
+
+ poll_id = poll.save()
+
+ self.assertEqual(Poll.OPEN, Poll.get(poll_id).status)
+
+ def test_is_open(self):
+
+ name = 'nombre de encuesta'
+ data = {'name': name}
+
+ poll = Poll(data=data)
+
+ self.assertTrue(poll.is_open())
+
+ poll.status = Poll.CLOSED
+
+ self.assertFalse(poll.is_open())
+
+
+class PollFormTests(MongoTestCase):
+
+ def test_save(self):
+ self.assertEqual(0, self.db.polls.count())
+
+ name = 'nombre de encuesta'
+ data = {'name': name, 'status': Poll.OPEN}
+
+ form = PollAddForm(data=data)
+
+ self.assertTrue(form.is_valid())
+
+ poll = form.save()
+
+ self.assertIsNotNone(poll)
+ self.assertIsInstance(poll, Poll)
+ self.assertEqual(1, self.db.polls.count())
+
+ def test_no_valid_data(self):
+ form = PollAddForm()
+
+ self.assertFalse(form.is_valid())
diff --git a/webapp/polls/tests/structure_tests.py b/webapp/polls/tests/structure_tests.py
new file mode 100644
index 0000000..0c3ef4d
--- /dev/null
+++ b/webapp/polls/tests/structure_tests.py
@@ -0,0 +1,219 @@
+# -*- encoding: utf-8 -*-
+from polls.models import Poll, Group, Field, Structure
+
+from utils.test import MongoTestCase
+
+
+class StructureTests(MongoTestCase):
+
+ def setUp(self):
+ poll = Poll(data={'name': 'name'})
+ poll_id = poll.save()
+
+ self.poll = Poll.get(id=poll_id)
+
+ self.data = {
+ 'poll_id': 'POLL_ID',
+ 'groups': {
+ '0': {
+ 'name': 'group_0',
+ 'fields': {
+ '0': {
+ 'widget_type': Field.TextInput,
+ 'name': 'field_0_0'
+ }
+ }
+ }
+ }
+ }
+
+ def test_structure(self):
+ data = {
+ 'groups': {
+ '0': {
+ 'name': 'group_0',
+ 'fields': {
+ '0': {
+ 'widget_type': Field.TextInput,
+ 'name': 'field_0_0'
+ },
+ '1': {
+ 'widget_type': Field.RadioButton,
+ 'name': 'field_0_1'
+ }
+ },
+ },
+ '1': {
+ 'name': 'group_1',
+ 'fields': {
+ '0': {
+ 'widget_type': Field.MultipleCheckBox,
+ 'name': 'field_1_0'
+ },
+ '1': {
+ 'widget_type': Field.DropDownList,
+ 'name': 'field_1_1'
+ }
+ },
+ }
+ }
+ }
+
+ structure = Structure(data=data, poll=self.poll)
+ self.assertEqual(self.poll, structure.poll)
+
+ group_0 = structure.groups[0]
+ self.assertEqual(self.poll, group_0.poll)
+ self.assertEqual(0, group_0.order)
+ self.assertEqual('group_0', group_0.name)
+ self.assertEqual(2, len(group_0.fields))
+
+ group_1 = structure.groups[1]
+ self.assertEqual(self.poll, group_1.poll)
+ self.assertEqual(1, group_1.order)
+ self.assertEqual('group_1', group_1.name)
+ self.assertEqual(2, len(group_1.fields))
+
+ field_0_0 = group_0.fields[0]
+ self.assertEqual(self.poll, field_0_0.poll)
+ self.assertEqual(0, field_0_0.order)
+ self.assertEqual('field_0_0', field_0_0.name)
+ self.assertEqual('TextInput', field_0_0.widget_type)
+
+ field_0_1 = group_0.fields[1]
+ self.assertEqual(self.poll, field_0_1.poll)
+ self.assertEqual(1, field_0_1.order)
+ self.assertEqual('field_0_1', field_0_1.name)
+ self.assertEqual('RadioButton', field_0_1.widget_type)
+
+ field_1_0 = group_1.fields[0]
+ self.assertEqual(self.poll, field_1_0.poll)
+ self.assertEqual(0, field_1_0.order)
+ self.assertEqual('field_1_0', field_1_0.name)
+ self.assertEqual('MultipleCheckBox', field_1_0.widget_type)
+
+ field_1_1 = group_1.fields[1]
+ self.assertEqual(self.poll, field_1_1.poll)
+ self.assertEqual(1, field_1_1.order)
+ self.assertEqual('field_1_1', field_1_1.name)
+ self.assertEqual('DropDownList', field_1_1.widget_type)
+
+ def test_is_valid(self):
+ data = {
+ 'groups': {
+ '0': {
+ 'name': 'group_0',
+ 'fields': {
+ '0': { # has no options for a widget_type that needs
+ 'widget_type': 'RadioButton',
+ 'name': 'field_0_0'
+ },
+ }
+ },
+ '1': {
+ 'name': '', # has no name
+ 'fields': {} # has no fields
+ }
+ }
+ }
+
+ structure = Structure(data=data, poll=self.poll)
+
+ self.assertFalse(structure.is_valid())
+
+ field_0_0 = structure.groups[0].fields[0]
+
+ self.assertEqual(1, len(field_0_0.errors))
+
+ group_1 = structure.groups[1]
+
+ self.assertEqual(2, len(group_1.errors))
+
+ def test_invalid_structure(self):
+ data = {}
+
+ structure = Structure(data=data, poll=self.poll)
+
+ self.assertFalse(structure.is_valid())
+
+ self.assertEqual(1, len(structure.errors))
+
+ def test_save(self):
+
+ structure = Structure(data=self.data)
+
+ self.assertRaises(structure.ValidationError, structure.save)
+
+ structure = Structure(data=self.data, poll=self.poll)
+ structure.save()
+
+ self.assertEqual(1, self.db.structures.count())
+
+ def test_get(self):
+ structure = Structure(data=self.data, poll=self.poll)
+ structure_id = structure.save()
+
+ structure = Structure.get(id=structure_id)
+
+ self.assertEqual(self.poll.id, structure.poll.id)
+
+ def test_to_python(self):
+
+ expected = {
+ 'groups': {
+ '0': {
+ 'name': "group name",
+ 'fields': {
+ '0': {
+ 'name': "field_0_0",
+ 'widget_type': Field.TextInput,
+ 'options': {
+ '131212': {
+ 'text': "text"
+ }
+ }
+ }
+ }
+ },
+ '1': {
+ 'name': "group name",
+ 'fields': {
+ '0': {
+ 'name': "field_1_0",
+ 'widget_type': Field.TextInput,
+ 'options': {
+ '131212': {
+ 'text': "text"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ structure = Structure(poll=self.poll)
+
+ field = Field({
+ 'name': "field_0_0",
+ 'widget_type': Field.TextInput,
+ 'order': 0
+ })
+ field.add_options({'131212': {'text': "text"}})
+
+ group = Group({'name': "group name", 'order': 0})
+ group.add_field(field, field.order)
+ structure.add_group(group, group.order)
+
+ field = Field({
+ 'name': "field_1_0",
+ 'widget_type': Field.TextInput,
+ 'order': 0
+ })
+ field.add_options({'131212': {'text': "text"}})
+
+ group = Group({'name': "group name", 'order': 1})
+ group.add_field(field, field.order)
+ structure.add_group(group, group.order)
+
+ self.assertEqual(expected, structure.to_python())
diff --git a/webapp/polls/urls.py b/webapp/polls/urls.py
index 2c5e2ef..aff1ae0 100644
--- a/webapp/polls/urls.py
+++ b/webapp/polls/urls.py
@@ -4,7 +4,18 @@ from polls.views import *
urlpatterns = patterns('',
- url(r'^builder/$', BuilderView.as_view(), name="builder"),
- url(r'^success/(?P<file_name>[a-zA-Z0-9_.-]+)/$',
- SucessView.as_view(), name="success"),
+ # Poll
+ url(r'^$', PollListView.as_view(), name="list"),
+ url(r'^add/$', PollFormView.as_view(), name="add"),
+ url(r'^edit/(?P<id>[0-9A-Fa-f]{24})/$',
+ PollFormView.as_view(), name="edit"),
+
+ # Poll structure
+ url(r'^structure/(?P<poll_id>[0-9A-Fa-f]{24})/$',
+ StructureFormView.as_view(), name="structure.builder"),
+ url(r'^download/(?P<poll_id>[0-9A-Fa-f]{24})/$',
+ 'polls.views.download_poll', name="download"),
+
+ url(r'^option/thumb/(?P<poll_id>[0-9A-Fa-f]{24})/(?P<img_name>.*)/$',
+ 'polls.views.opt_thumb')
)
diff --git a/webapp/polls/views.py b/webapp/polls/views.py
index 1924a4b..5e14e0c 100644
--- a/webapp/polls/views.py
+++ b/webapp/polls/views.py
@@ -1,20 +1,31 @@
-import io
-import json
+# -*- encoding: utf-8 -*-
+import os
+
from datetime import datetime
-from django.conf import settings
+from pymongo import ASCENDING
+
from django.views.generic.base import TemplateView
+from django.views.generic.list import ListView
from django.utils.datastructures import DotExpandedDict
from django.utils.safestring import SafeUnicode
-from django.http import HttpResponse, HttpResponseRedirect
-from django.core.servers.basehttp import FileWrapper
+from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.core.urlresolvers import reverse
+from django.views.generic.edit import FormView
+from django.contrib import messages
+from django.core.files.uploadedfile import InMemoryUploadedFile
+from django.shortcuts import render_to_response
+from django.conf import settings
+
+from utils.forms import BadFormValidation
+from utils.data_structure import dict_merge
-from polls.models import WIDGET_TYPES, Structure
+from polls.models import WIDGET_TYPES, Structure, Poll
+from polls.forms import PollAddForm
__all__ = [
- 'BuilderView', 'SucessView',
+ 'StructureFormView', 'PollFormView', 'PollListView',
]
@@ -32,11 +43,11 @@ def clean_data(value):
value = [clean_data(value_) for value_ in value]
return value
- value = value.strip(' ')
+ if not isinstance(value, InMemoryUploadedFile):
+ value = value.strip(' ')
- # TODO: Organizar esto con Flavio.
- #if value == '':
- # value = None
+ if value == '':
+ value = None
if value == 'True' or value in [u'on']:
value = True
if value == 'False':
@@ -48,124 +59,185 @@ def clean_data(value):
return value
-class SucessView(TemplateView):
+class PollFormView(FormView):
- template_name = "sucess.html"
+ template_name = "poll-form.html"
+ form_class = PollAddForm
- def get(self, request, *args, **kwargs):
- return self.render_to_response(self.get_context_data())
+ def get_context_data(self, **kwargs):
+ context = super(PollFormView, self).get_context_data(**kwargs)
+ form = context['form']
+
+ # If id and Wrong id or poll is not open => 404
+ _id = self.kwargs.get('id', None)
+ poll = Poll.get(id=_id) if _id else Poll()
+ if _id and not poll:
+ raise Http404()
+ if poll and not poll.is_open():
+ if not self.request.user.is_superuser:
+ raise Http404()
+
+ poll = Poll(form.data) if form.is_bound else poll
+ context.update(
+ {"poll": poll, "STATUS_CHOICES": Poll.status_choices()})
+ return context
+
+ def form_valid(self, form):
+ try:
+ poll = form.save()
+ except BadFormValidation:
+ msg = u'Ocurrió un error, no se guardó la encuesta.'
+ messages.add_message(self.request, messages.ERROR, msg)
+ return self.render_to_response(self.get_context_data(form=form))
+ # TODO: Logear
+ except Exception:
+ pass
+ # TODO: Logear
+ else:
+ msg = u'La encuesta: "%s" fué guardada correctamente.' % poll.name
+ messages.add_message(self.request, messages.SUCCESS, msg)
+
+ if self.request.GET.get('continue', None):
+ return HttpResponseRedirect(
+ reverse(
+ 'polls:structure.builder',
+ kwargs={'poll_id': str(poll.id)}
+ )
+ )
+ else:
+ return HttpResponseRedirect(
+ reverse(
+ 'polls:edit',
+ kwargs={'id': str(poll.id)}
+ )
+ )
- def post(self, request, *args, **kwargs):
- file_name = kwargs.get('file_name', None)
- if file_name:
- file_path = '%soutput/%s.json' % (settings.MEDIA_ROOT, file_name)
- # Download json file
- f = file(file_path)
- response = HttpResponse(
- FileWrapper(f), content_type='application/json')
- response['Content-Disposition'] = (
- 'attachment; filename=%s.json' % file_name)
- return response
+class PollListView(ListView):
+ template_name = "poll-list.html"
+ context_object_name = "polls"
-class BuilderView(TemplateView):
+ def get_queryset(self, *args, **kwargs):
+ return Poll.all(
+ sort=[('status', ASCENDING), ('name', ASCENDING)])
- template_name = "builder.html"
+
+class StructureFormView(TemplateView):
+
+ template_name = "poll-structure-form.html"
+
+ def get(self, request, *args, **kwargs):
+ context = self.get_context_data()
+
+ context.update({'structure': self.poll.structure})
+
+ return self.render_to_response(context)
def get_context_data(self, **kwargs):
+ context = super(StructureFormView, self).get_context_data(**kwargs)
- context = super(BuilderView, self).get_context_data(**kwargs)
- context.update({'WIDGET_TYPES': WIDGET_TYPES})
-
- if self.request.method == "GET":
- fixed_data = {
- 'groups': {
- '0': {
- 'name': '',
- 'fields': {
- '0': {
- 'widget_type': '',
- 'name': ''
- },
- '1': {
- 'widget_type': '',
- 'name': '',
- 'options': {}
- },
- '2': {
- 'widget_type': '',
- 'name': '',
- 'options': {}
- },
- '3': {
- 'widget_type': '',
- 'name': '',
- 'options': {}
- },
- '4': {
- 'widget_type': '',
- 'name': '',
- 'options': {}
- },
- '5': {
- 'widget_type': '',
- 'name': '',
- 'options': {}
- }
-
- },
- },
- '1': {
- 'name': '',
- 'fields': {
- '0': {
- 'widget_type': '',
- 'name': '',
- 'options': {}
- },
- '1': {
- 'widget_type': '',
- 'name': '',
- 'options': {}
- }
- },
- }
- }
- }
- context.update({
- 'structure': Structure(data=fixed_data)
- })
+ # If id and Wrong id or poll is not open => 404
+ _id = _id = self.kwargs.get('poll_id', None)
+ self.poll = Poll.get(id=_id) if _id else None
+ if _id and not self.poll:
+ raise Http404()
+ if self.poll and not self.poll.is_open():
+ raise Http404()
+ context.update({'WIDGET_TYPES': WIDGET_TYPES, 'poll': self.poll})
return context
def post(self, request, *args, **kwargs):
context = self.get_context_data()
+ data = {}
+
+ data_post = DotExpandedDict(dict(request.POST.lists()))
+ data_files = DotExpandedDict(dict(request.FILES.lists()))
+
+ data = dict_merge(data_post, data_files)
- data = DotExpandedDict(dict(request.POST.lists()))
for key, value in data.items():
data[key] = clean_data(value)
- structure = Structure(data={"groups": data['groups']})
+ _id = data.get('id', None)
+ structure = Structure(
+ data={"groups": data.get('groups', {}), 'id': _id},
+ poll=self.poll
+ )
if structure.is_valid():
- _json = json.dumps(
- data['groups'],
- sort_keys=True,
- indent=4,
- separators=(',', ': '),
- ensure_ascii=False
- )
- file_name = datetime.now().strftime("%d_%m_%Y_%H_%M_%S")
- file_path = '%soutput/%s.json' % (settings.MEDIA_ROOT, file_name)
-
- # Saving json file
- with io.open(file_path, 'w', encoding='utf-8') as f:
- f.write(unicode(_json))
- f.close()
-
- return HttpResponseRedirect(
- reverse('polls:success', kwargs={"file_name": file_name}))
+ try:
+ structure.save_image_options(tmp=False)
+ structure.save()
+ except Exception:
+ msg = u'Ocurrió un error, no se guardó la estructura.'
+ messages.add_message(self.request, messages.ERROR, msg)
+
+ # Rollback img options to tmp directory
+ if not structure.id:
+ structure.rollback()
+ else:
+ messages.add_message(
+ self.request,
+ messages.SUCCESS,
+ u'La estructura de la encuesta "%s" fué'
+ ' guardada correctamente.' % self.poll.name
+ )
+
+ return HttpResponseRedirect(
+ reverse(
+ 'polls:structure.builder',
+ kwargs={"poll_id": str(self.poll.id)}
+ )
+ )
+ else:
+ structure.save_image_options(tmp=True)
+ context.update({'errors': structure.errors})
context.update({'structure': structure})
return self.render_to_response(context)
+
+
+def download_poll(request, poll_id=None):
+
+ # Wrong id => 404
+ _id = poll_id
+ poll = Poll.get(id=_id)
+ if not _id or not poll:
+ raise Http404()
+ elif poll and poll.is_open():
+ raise Http404()
+
+ file_name = datetime.now().strftime("%d_%m_%Y_%H_%M_%S")
+
+ # Download json file
+ response = HttpResponse(
+ poll.to_json(), content_type='application/json')
+ response['Content-Disposition'] = (
+ 'attachment; filename=%s.json' % file_name)
+ return response
+
+
+def opt_thumb(request, poll_id, img_name):
+
+ tmp_path = settings.IMAGE_OPTIONS_ROOT + '/%s/tmp/%s' % (
+ poll_id, img_name)
+ path = settings.IMAGE_OPTIONS_ROOT + '/%s/%s' % (
+ poll_id, img_name)
+
+ src = ''
+
+ if os.path.exists(tmp_path):
+ src = tmp_path
+ media_url = settings.IMAGE_OPTIONS_MEDIA_URL + '/%s/tmp/%s' % (
+ poll_id, img_name)
+ elif os.path.exists(path):
+ src = path
+ media_url = settings.IMAGE_OPTIONS_MEDIA_URL + '/%s/%s' % (
+ poll_id, img_name)
+
+ return render_to_response(
+ 'image_option_thumbnail.html',
+ {"img": open(src), "img_src": media_url}
+ )
diff --git a/webapp/requirements b/webapp/requirements
index 4a14aef..039c8bd 100644
--- a/webapp/requirements
+++ b/webapp/requirements
@@ -3,4 +3,8 @@ pep8==1.4.2
pyflakes==0.6.1
nose==1.2.1
django-nose==1.1
-fabric==1.5.3 \ No newline at end of file
+fabric==1.5.3
+pymongo==2.4.2
+django-jasmine==0.3.2
+sorl-thumbnail==11.12
+PIL==1.1.7 \ No newline at end of file
diff --git a/webapp/utils/data_structure.py b/webapp/utils/data_structure.py
new file mode 100644
index 0000000..730ba7e
--- /dev/null
+++ b/webapp/utils/data_structure.py
@@ -0,0 +1,7 @@
+def dict_merge(d1, d2):
+ for k1, v1 in d1.iteritems():
+ if not k1 in d2:
+ d2[k1] = v1
+ elif isinstance(v1, dict):
+ dict_merge(v1, d2[k1])
+ return d2
diff --git a/webapp/utils/decorators.py b/webapp/utils/decorators.py
new file mode 100644
index 0000000..05184f9
--- /dev/null
+++ b/webapp/utils/decorators.py
@@ -0,0 +1,98 @@
+from functools import wraps
+
+from django.conf import settings
+from django.utils.importlib import import_module
+from django.core.exceptions import ImproperlyConfigured
+from django.core.urlresolvers import RegexURLPattern, RegexURLResolver
+from django.contrib.auth.decorators import login_required
+
+
+__all__ = [
+ 'decorator_include',
+ 'user_account_required'
+]
+
+
+class DecoratedPatterns(object):
+ """
+ A wrapper for an urlconf that applies a decorator to all its views.
+ """
+ def __init__(self, urlconf_name, decorators):
+ self.urlconf_name = urlconf_name
+ try:
+ iter(decorators)
+ except TypeError:
+ decorators = [decorators]
+ self.decorators = decorators
+ if not isinstance(urlconf_name, basestring):
+ self._urlconf_module = self.urlconf_name
+ else:
+ self._urlconf_module = None
+
+ def decorate_pattern(self, pattern):
+ if isinstance(pattern, RegexURLResolver):
+ regex = pattern.regex.pattern
+ urlconf_module = pattern.urlconf_name
+ default_kwargs = pattern.default_kwargs
+ namespace = pattern.namespace
+ app_name = pattern.app_name
+ urlconf = DecoratedPatterns(urlconf_module, self.decorators)
+ decorated = RegexURLResolver(
+ regex, urlconf, default_kwargs,
+ app_name, namespace
+ )
+ else:
+ callback = pattern.callback
+ for decorator in reversed(self.decorators):
+ callback = decorator(callback)
+ decorated = RegexURLPattern(
+ pattern.regex.pattern,
+ callback,
+ pattern.default_args,
+ pattern.name
+ )
+ return decorated
+
+ def _get_urlconf_module(self):
+ if self._urlconf_module is None:
+ self._urlconf_module = import_module(self.urlconf_name)
+ return self._urlconf_module
+ urlconf_module = property(_get_urlconf_module)
+
+ def _get_urlpatterns(self):
+ try:
+ patterns = self.urlconf_module.urlpatterns
+ except AttributeError:
+ patterns = self.urlconf_module
+ return [self.decorate_pattern(pattern) for pattern in patterns]
+ urlpatterns = property(_get_urlpatterns)
+
+ def __getattr__(self, name):
+ return getattr(self.urlconf_module, name)
+
+
+def decorator_include(decorators, arg, namespace=None, app_name=None):
+ """
+ Works like ``django.conf.urls.defaults.include`` but takes a view decorator
+ or an iterable of view decorators as the first argument and applies them,
+ in reverse order, to all views in the included urlconf.
+ """
+ if isinstance(arg, tuple):
+ if namespace:
+ raise ImproperlyConfigured(
+ 'Cannot override the namespace for a dynamic '
+ 'module that provides a namespace'
+ )
+ urlconf, app_name, namespace = arg
+ else:
+ urlconf = arg
+ decorated_urlconf = DecoratedPatterns(urlconf, decorators)
+ return (decorated_urlconf, app_name, namespace)
+
+
+def user_account_required(view_function):
+ @wraps(view_function)
+ @login_required(login_url=settings.LOGIN_URL)
+ def decorator(*args, **kwargs):
+ return view_function(*args, **kwargs)
+ return decorator
diff --git a/webapp/utils/forms.py b/webapp/utils/forms.py
new file mode 100644
index 0000000..6701024
--- /dev/null
+++ b/webapp/utils/forms.py
@@ -0,0 +1,2 @@
+class BadFormValidation(Exception):
+ pass
diff --git a/webapp/utils/mongo_connection.py b/webapp/utils/mongo_connection.py
new file mode 100644
index 0000000..bae5dc2
--- /dev/null
+++ b/webapp/utils/mongo_connection.py
@@ -0,0 +1,169 @@
+import pymongo
+from pymongo import Connection, ReplicaSetConnection, uri_parser
+
+
+__all__ = ['ConnectionError', 'connect', 'register_connection',
+ 'DEFAULT_CONNECTION_NAME']
+
+
+DEFAULT_CONNECTION_NAME = 'default'
+
+
+class ConnectionError(Exception):
+ pass
+
+
+_connection_settings = {}
+_connections = {}
+_dbs = {}
+
+
+def register_connection(alias, name, host='localhost', port=27017,
+ is_slave=False, read_preference=False, slaves=None,
+ username=None, password=None, **kwargs):
+ """Add a connection.
+
+ :param alias: the name that will be used to refer to this connection
+ throughout MongoEngine
+ :param name: the name of the specific database to use
+ :param host: the host name of the :program:`mongod` instance to connect to
+ :param port: the port that the :program:`mongod` instance is running on
+ :param is_slave: whether the connection can act as a slave ** Depreciated
+ pymongo 2.0.1+
+ :param read_preference: The read preference for the collection ** Added
+ pymongo 2.1
+ :param slaves: a list of aliases of slave connections; each of these must
+ be a registered connection that has :attr:`is_slave` set to ``True``
+ :param username: username to authenticate with
+ :param password: password to authenticate with
+ :param kwargs: allow ad-hoc parameters to be passed into the pymongo driver
+
+ """
+ global _connection_settings
+
+ conn_settings = {
+ 'name': name,
+ 'host': host,
+ 'port': port,
+ 'is_slave': is_slave,
+ 'slaves': slaves or [],
+ 'username': username,
+ 'password': password,
+ 'read_preference': read_preference
+ }
+
+ # Handle uri style connections
+ if "://" in host:
+ uri_dict = uri_parser.parse_uri(host)
+ if uri_dict.get('database') is None:
+ raise ConnectionError("If using URI style connection include "
+ "database name in string")
+ conn_settings.update({
+ 'host': host,
+ 'name': uri_dict.get('database'),
+ 'username': uri_dict.get('username'),
+ 'password': uri_dict.get('password'),
+ 'read_preference': read_preference,
+ })
+ if "replicaSet" in host:
+ conn_settings['replicaSet'] = True
+
+ conn_settings.update(kwargs)
+ _connection_settings[alias] = conn_settings
+
+
+def disconnect(alias=DEFAULT_CONNECTION_NAME):
+ global _connections
+ global _dbs
+
+ if alias in _connections:
+ get_connection(alias=alias).disconnect()
+ del _connections[alias]
+ if alias in _dbs:
+ del _dbs[alias]
+
+
+def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
+ global _connections
+ # Connect to the database if not already connected
+ if reconnect:
+ disconnect(alias)
+
+ if alias not in _connections:
+ if alias not in _connection_settings:
+ msg = 'Connection with alias "%s" has not been defined' % alias
+ if alias == DEFAULT_CONNECTION_NAME:
+ msg = 'You have not defined a default connection'
+ raise ConnectionError(msg)
+ conn_settings = _connection_settings[alias].copy()
+
+ if hasattr(pymongo, 'version_tuple'): # Support for 2.1+
+ conn_settings.pop('name', None)
+ conn_settings.pop('slaves', None)
+ conn_settings.pop('is_slave', None)
+ conn_settings.pop('username', None)
+ conn_settings.pop('password', None)
+ else:
+ # Get all the slave connections
+ if 'slaves' in conn_settings:
+ slaves = []
+ for slave_alias in conn_settings['slaves']:
+ slaves.append(get_connection(slave_alias))
+ conn_settings['slaves'] = slaves
+ conn_settings.pop('read_preference', None)
+
+ connection_class = Connection
+ if 'replicaSet' in conn_settings:
+ conn_settings['hosts_or_uri'] = conn_settings.pop('host', None)
+ # Discard port since it can't be used on ReplicaSetConnection
+ conn_settings.pop('port', None)
+ # Discard replicaSet if not base string
+ if not isinstance(conn_settings['replicaSet'], basestring):
+ conn_settings.pop('replicaSet', None)
+ connection_class = ReplicaSetConnection
+
+ try:
+ _connections[alias] = connection_class(**conn_settings)
+ except Exception, e:
+ raise ConnectionError(
+ "Cannot connect to database %s :\n%s" % (alias, e))
+ return _connections[alias]
+
+
+def get_db(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
+ global _dbs
+ if reconnect:
+ disconnect(alias)
+
+ if alias not in _dbs:
+ conn = get_connection(alias)
+ conn_settings = _connection_settings[alias]
+ _dbs[alias] = conn[conn_settings['name']]
+ # Authenticate if necessary
+ if conn_settings['username'] and conn_settings['password']:
+ _dbs[alias].authenticate(conn_settings['username'],
+ conn_settings['password'])
+ return _dbs[alias]
+
+
+def connect(db, alias=DEFAULT_CONNECTION_NAME, **kwargs):
+ """Connect to the database specified by the 'db' argument.
+
+ Connection settings may be provided here as well if the database is not
+ running on the default port on localhost. If authentication is needed,
+ provide username and password arguments as well.
+
+ Multiple databases are supported by using aliases. Provide a separate
+ `alias` to connect to a different instance of :program:`mongod`.
+
+ .. versionchanged:: 0.6 - added multiple database support.
+ """
+ global _connections
+ if alias not in _connections:
+ register_connection(alias, db, **kwargs)
+
+ return get_connection(alias)
+
+# Support old naming convention
+_get_connection = get_connection
+_get_db = get_db
diff --git a/webapp/utils/test.py b/webapp/utils/test.py
new file mode 100644
index 0000000..98c4209
--- /dev/null
+++ b/webapp/utils/test.py
@@ -0,0 +1,56 @@
+import StringIO
+import Image
+
+from django.conf import settings
+from django.test import TestCase
+from django.core.files.uploadedfile import InMemoryUploadedFile
+
+from utils.mongo_connection import connect
+
+
+class MongoTestCase(TestCase):
+
+ def __init__(self, methodName='runtest'):
+ db_name = 'test_%s' % settings.MONGO_SETTINGS['NAME']
+ self.connection = connect(
+ db_name,
+ settings.MONGO_SETTINGS['ALIAS'],
+ host=settings.MONGO_SETTINGS['HOST'],
+ port=settings.MONGO_SETTINGS['PORT'],
+ )
+ self.db = self.connection[db_name]
+ super(MongoTestCase, self).__init__(methodName)
+
+ def _post_teardown(self):
+ for collection_name in self.db.collection_names():
+ if collection_name != 'system.indexes':
+ self.db.drop_collection(collection_name)
+
+ super(MongoTestCase, self)._post_teardown()
+
+
+def mock_text_file():
+ io = StringIO.StringIO()
+ io.write('foo')
+ text_file = InMemoryUploadedFile(io, None, 'foo.txt', 'text', io.len, None)
+ text_file.seek(0)
+ return text_file
+
+
+def mock_image_file(format="jpeg", size=(200, 200)):
+ io = StringIO.StringIO()
+ color = (255, 0, 0, 0)
+ image = Image.new("RGBA", size, color)
+ image.save(io, format=format.upper())
+ io.seek(0)
+ return io
+
+
+def mock_in_memory_image(name='foo.jpg', format="jpeg", size=(200, 200)):
+ io = mock_image_file(format, size)
+ content_type = "Image/%s" % format.lower()
+ image_in_memory = InMemoryUploadedFile(
+ io, None, name, content_type, io.len, None
+ )
+ image_in_memory.seek(0)
+ return image_in_memory
diff --git a/webapp/webapp/env_settings.py.sample b/webapp/webapp/env_settings.py.sample
index b0967ac..eee0844 100644
--- a/webapp/webapp/env_settings.py.sample
+++ b/webapp/webapp/env_settings.py.sample
@@ -1,6 +1,25 @@
+from utils.mongo_connection import register_connection
+
+
DEBUG = False
TEMPLATE_DEBUG = DEBUG
# Hosts/domain names that are valid for this site; required if DEBUG is False
# See https://docs.djangoproject.com/en//ref/settings/#allowed-hosts
-ALLOWED_HOSTS = ["domain.com"] \ No newline at end of file
+ALLOWED_HOSTS = ["domain.com"]
+
+MONGO_SETTINGS = {
+ 'ALIAS': 'default',
+ 'NAME': 'dev_polls',
+ 'USER': '',
+ 'PASSWORD': '',
+ 'HOST': '127.0.0.1',
+ 'PORT': 27017,
+}
+
+register_connection(
+ alias=MONGO_SETTINGS['ALIAS'],
+ name=MONGO_SETTINGS['NAME'],
+ host=MONGO_SETTINGS['HOST'],
+ port=MONGO_SETTINGS['PORT'],
+)
diff --git a/webapp/webapp/media/output/empty b/webapp/webapp/media/image_options/empty
index e69de29..e69de29 100644
--- a/webapp/webapp/media/output/empty
+++ b/webapp/webapp/media/image_options/empty
diff --git a/webapp/webapp/settings.py b/webapp/webapp/settings.py
index bbc2111..3232cd2 100644
--- a/webapp/webapp/settings.py
+++ b/webapp/webapp/settings.py
@@ -16,7 +16,7 @@ MANAGERS = ADMINS
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': 'database.db',
+ 'NAME': PROJECT_ROOT + '/../db/database.db',
}
}
@@ -96,12 +96,20 @@ MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
- #'django.contrib.auth.middleware.AuthenticationMiddleware',
- #'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
# Uncomment the next line for simple clickjacking protection:
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
+TEMPLATE_CONTEXT_PROCESSORS = (
+ 'django.core.context_processors.media',
+ 'django.core.context_processors.static',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ 'django.core.context_processors.request',
+)
+
ROOT_URLCONF = 'webapp.urls'
# Python dotted path to the WSGI application used by Django's runserver.
@@ -115,16 +123,27 @@ INSTALLED_APPS = (
# Test Runner
'django_nose',
+ 'django_jasmine',
+ # Project
'polls',
- #'django.contrib.auth',
- #'django.contrib.contenttypes',
- #'django.contrib.sessions',
+ 'custom_admin',
+ 'accounts',
+
+ # Django auth
+ 'django.contrib.contenttypes',
+ 'django.contrib.auth',
+
+ # Django admin
+ 'django.contrib.sessions',
+ 'django.contrib.admin',
+
+ # third party
+ 'sorl.thumbnail',
+
#'django.contrib.sites',
#'django.contrib.messages',
#'django.contrib.staticfiles',
- # Uncomment the next line to enable the admin:
- # 'django.contrib.admin',
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
)
@@ -174,6 +193,15 @@ LOGGING = {
}
}
+LOGIN_REDIRECT_URL = '/'
+
+JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/../js_tests/'
+
+IMAGE_OPTIONS_ROOT = MEDIA_ROOT + 'image_options'
+
+IMAGE_OPTIONS_MEDIA_URL = MEDIA_URL + 'image_options'
+
+THUMBNAIL_DEBUG = True
try:
from env_settings import *
diff --git a/webapp/webapp/static/css/bootstrap-fileupload.css b/webapp/webapp/static/css/bootstrap-fileupload.css
new file mode 100755
index 0000000..eec90a7
--- /dev/null
+++ b/webapp/webapp/static/css/bootstrap-fileupload.css
@@ -0,0 +1,132 @@
+/*!
+ * Bootstrap v2.3.1-j6
+ *
+ * Copyright 2012 Twitter, Inc
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Designed and built with all the love in the world by @mdo and @fat, extended by @ArnoldDaniels.
+ */
+.clearfix {
+ *zoom: 1;
+}
+.clearfix:before,
+.clearfix:after {
+ display: table;
+ content: "";
+ line-height: 0;
+}
+.clearfix:after {
+ clear: both;
+}
+.hide-text {
+ font: 0/0 a;
+ color: transparent;
+ text-shadow: none;
+ background-color: transparent;
+ border: 0;
+}
+.input-block-level {
+ display: block;
+ width: 100%;
+ min-height: 30px;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+.btn-file {
+ overflow: hidden;
+ position: relative;
+ vertical-align: middle;
+}
+.btn-file > input {
+ position: absolute;
+ top: 0;
+ right: 0;
+ margin: 0;
+ opacity: 0;
+ filter: alpha(opacity=0);
+ transform: translate(-300px, 0) scale(4);
+ font-size: 23px;
+ direction: ltr;
+ cursor: pointer;
+}
+.fileupload {
+ margin-bottom: 9px;
+}
+.fileupload .uneditable-input {
+ display: inline-block;
+ margin-bottom: 0px;
+ vertical-align: middle;
+ cursor: text;
+}
+.fileupload .thumbnail {
+ overflow: hidden;
+ display: inline-block;
+ margin-bottom: 5px;
+ vertical-align: middle;
+ text-align: center;
+}
+.fileupload .thumbnail > img {
+ display: inline-block;
+ vertical-align: middle;
+ max-height: 100%;
+}
+.fileupload .btn {
+ vertical-align: middle;
+}
+.fileupload-exists .fileupload-new,
+.fileupload-new .fileupload-exists {
+ display: none;
+}
+.fileupload-inline .fileupload-controls {
+ display: inline;
+}
+.fileupload-new .input-append .btn-file {
+ -webkit-border-radius: 0 3px 3px 0;
+ -moz-border-radius: 0 3px 3px 0;
+ border-radius: 0 3px 3px 0;
+}
+.thumbnail-borderless .thumbnail {
+ border: none;
+ padding: 0;
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
+}
+.fileupload-new.thumbnail-borderless .thumbnail {
+ border: 1px solid #ddd;
+}
+.control-group.warning .fileupload .uneditable-input {
+ color: #a47e3c;
+ border-color: #a47e3c;
+}
+.control-group.warning .fileupload .fileupload-preview {
+ color: #a47e3c;
+}
+.control-group.warning .fileupload .thumbnail {
+ border-color: #a47e3c;
+}
+.control-group.error .fileupload .uneditable-input {
+ color: #b94a48;
+ border-color: #b94a48;
+}
+.control-group.error .fileupload .fileupload-preview {
+ color: #b94a48;
+}
+.control-group.error .fileupload .thumbnail {
+ border-color: #b94a48;
+}
+.control-group.success .fileupload .uneditable-input {
+ color: #468847;
+ border-color: #468847;
+}
+.control-group.success .fileupload .fileupload-preview {
+ color: #468847;
+}
+.control-group.success .fileupload .thumbnail {
+ border-color: #468847;
+}
diff --git a/webapp/webapp/static/css/custom_admin.css b/webapp/webapp/static/css/custom_admin.css
new file mode 100644
index 0000000..943fd6c
--- /dev/null
+++ b/webapp/webapp/static/css/custom_admin.css
@@ -0,0 +1,35 @@
+[class^="icon-"],
+[class*=" icon-"] {
+ display: inline-block;
+ width: 14px;
+ height: 14px;
+ margin-top: 1px;
+ *margin-right: .3em;
+ line-height: 14px;
+ vertical-align: text-top;
+ background-image: url("../img/glyphicons-halflings.png");
+ background-position: 14px 14px;
+ background-repeat: no-repeat;
+}
+
+/* White icons with optional class, or on hover/active states of certain elements */
+
+.icon-white,
+.nav > .active > a > [class^="icon-"],
+.nav > .active > a > [class*=" icon-"],
+.dropdown-menu > li > a:hover > [class^="icon-"],
+.dropdown-menu > li > a:hover > [class*=" icon-"],
+.dropdown-menu > .active > a > [class^="icon-"],
+.dropdown-menu > .active > a > [class*=" icon-"] {
+ background-image: url("../img/glyphicons-halflings-white.png");
+}
+
+.icon-chevron-left {
+ background-position: -432px -72px;
+}
+
+#header{ background-color: #333940; border-bottom: solid 3px #999; }
+
+#branding h1{ color: #fff; }
+.module h2, .module caption, .inline-group h2 { background:#ccc bottom left repeat-x; color: #333940; }
+a.section:link, a.section:visited { color: #666666; } \ No newline at end of file
diff --git a/webapp/webapp/static/img/no_image.gif b/webapp/webapp/static/img/no_image.gif
new file mode 100644
index 0000000..d1ac542
--- /dev/null
+++ b/webapp/webapp/static/img/no_image.gif
Binary files differ
diff --git a/webapp/webapp/static/jasmine-jquery-latest.js b/webapp/webapp/static/jasmine-jquery-latest.js
new file mode 100644
index 0000000..752def8
--- /dev/null
+++ b/webapp/webapp/static/jasmine-jquery-latest.js
@@ -0,0 +1,288 @@
+var readFixtures = function() {
+ return jasmine.getFixtures().proxyCallTo_('read', arguments);
+};
+
+var preloadFixtures = function() {
+ jasmine.getFixtures().proxyCallTo_('preload', arguments);
+};
+
+var loadFixtures = function() {
+ jasmine.getFixtures().proxyCallTo_('load', arguments);
+};
+
+var setFixtures = function(html) {
+ jasmine.getFixtures().set(html);
+};
+
+var sandbox = function(attributes) {
+ return jasmine.getFixtures().sandbox(attributes);
+};
+
+var spyOnEvent = function(selector, eventName) {
+ jasmine.JQuery.events.spyOn(selector, eventName);
+}
+
+jasmine.getFixtures = function() {
+ return jasmine.currentFixtures_ = jasmine.currentFixtures_ || new jasmine.Fixtures();
+};
+
+jasmine.Fixtures = function() {
+ this.containerId = 'jasmine-fixtures';
+ this.fixturesCache_ = {};
+ this.fixturesPath = 'spec/javascripts/fixtures';
+};
+
+jasmine.Fixtures.prototype.set = function(html) {
+ this.cleanUp();
+ this.createContainer_(html);
+};
+
+jasmine.Fixtures.prototype.preload = function() {
+ this.read.apply(this, arguments);
+};
+
+jasmine.Fixtures.prototype.load = function() {
+ this.cleanUp();
+ this.createContainer_(this.read.apply(this, arguments));
+};
+
+jasmine.Fixtures.prototype.read = function() {
+ var htmlChunks = [];
+
+ var fixtureUrls = arguments;
+ for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) {
+ htmlChunks.push(this.getFixtureHtml_(fixtureUrls[urlIndex]));
+ }
+
+ return htmlChunks.join('');
+};
+
+jasmine.Fixtures.prototype.clearCache = function() {
+ this.fixturesCache_ = {};
+};
+
+jasmine.Fixtures.prototype.cleanUp = function() {
+ jQuery('#' + this.containerId).remove();
+};
+
+jasmine.Fixtures.prototype.sandbox = function(attributes) {
+ var attributesToSet = attributes || {};
+ return jQuery('<div id="sandbox" />').attr(attributesToSet);
+};
+
+jasmine.Fixtures.prototype.createContainer_ = function(html) {
+ var container;
+ if(html instanceof jQuery) {
+ container = jQuery('<div id="' + this.containerId + '" />');
+ container.html(html);
+ } else {
+ container = '<div id="' + this.containerId + '">' + html + '</div>'
+ }
+ jQuery('body').append(container);
+};
+
+jasmine.Fixtures.prototype.getFixtureHtml_ = function(url) {
+ if (typeof this.fixturesCache_[url] == 'undefined') {
+ this.loadFixtureIntoCache_(url);
+ }
+ return this.fixturesCache_[url];
+};
+
+jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function(relativeUrl) {
+ var self = this;
+ var url = this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl;
+ jQuery.ajax({
+ async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded
+ cache: false,
+ dataType: 'html',
+ url: url,
+ success: function(data) {
+ self.fixturesCache_[relativeUrl] = data;
+ },
+ error: function(jqXHR, status, errorThrown) {
+ throw Error('Fixture could not be loaded: ' + url + ' (status: ' + status + ', message: ' + errorThrown.message + ')');
+ }
+ });
+};
+
+jasmine.Fixtures.prototype.proxyCallTo_ = function(methodName, passedArguments) {
+ return this[methodName].apply(this, passedArguments);
+};
+
+
+jasmine.JQuery = function() {};
+
+jasmine.JQuery.browserTagCaseIndependentHtml = function(html) {
+ return jQuery('<div/>').append(html).html();
+};
+
+jasmine.JQuery.elementToString = function(element) {
+ return jQuery('<div />').append(element.clone()).html();
+};
+
+jasmine.JQuery.matchersClass = {};
+
+(function(namespace) {
+ var data = {
+ spiedEvents: {},
+ handlers: []
+ };
+
+ namespace.events = {
+ spyOn: function(selector, eventName) {
+ var handler = function(e) {
+ data.spiedEvents[[selector, eventName]] = e;
+ };
+ jQuery(selector).bind(eventName, handler);
+ data.handlers.push(handler);
+ },
+
+ wasTriggered: function(selector, eventName) {
+ return !!(data.spiedEvents[[selector, eventName]]);
+ },
+
+ cleanUp: function() {
+ data.spiedEvents = {};
+ data.handlers = [];
+ }
+ }
+})(jasmine.JQuery);
+
+(function(){
+ var jQueryMatchers = {
+ toHaveClass: function(className) {
+ return this.actual.hasClass(className);
+ },
+
+ toBeVisible: function() {
+ return this.actual.is(':visible');
+ },
+
+ toBeHidden: function() {
+ return this.actual.is(':hidden');
+ },
+
+ toBeSelected: function() {
+ return this.actual.is(':selected');
+ },
+
+ toBeChecked: function() {
+ return this.actual.is(':checked');
+ },
+
+ toBeEmpty: function() {
+ return this.actual.is(':empty');
+ },
+
+ toExist: function() {
+ return this.actual.size() > 0;
+ },
+
+ toHaveAttr: function(attributeName, expectedAttributeValue) {
+ return hasProperty(this.actual.attr(attributeName), expectedAttributeValue);
+ },
+
+ toHaveId: function(id) {
+ return this.actual.attr('id') == id;
+ },
+
+ toHaveHtml: function(html) {
+ return this.actual.html() == jasmine.JQuery.browserTagCaseIndependentHtml(html);
+ },
+
+ toHaveText: function(text) {
+ if (text && jQuery.isFunction(text.test)) {
+ return text.test(this.actual.text());
+ } else {
+ return this.actual.text() == text;
+ }
+ },
+
+ toHaveValue: function(value) {
+ return this.actual.val() == value;
+ },
+
+ toHaveData: function(key, expectedValue) {
+ return hasProperty(this.actual.data(key), expectedValue);
+ },
+
+ toBe: function(selector) {
+ return this.actual.is(selector);
+ },
+
+ toContain: function(selector) {
+ return this.actual.find(selector).size() > 0;
+ },
+
+ toBeDisabled: function(selector){
+ return this.actual.is(':disabled');
+ },
+
+ // tests the existence of a specific event binding
+ toHandle: function(eventName) {
+ var events = this.actual.data("events");
+ return events && events[eventName].length > 0;
+ },
+
+ // tests the existence of a specific event binding + handler
+ toHandleWith: function(eventName, eventHandler) {
+ var stack = this.actual.data("events")[eventName];
+ var i;
+ for (i = 0; i < stack.length; i++) {
+ if (stack[i].handler == eventHandler) {
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+
+ var hasProperty = function(actualValue, expectedValue) {
+ if (expectedValue === undefined) {
+ return actualValue !== undefined;
+ }
+ return actualValue == expectedValue;
+ };
+
+ var bindMatcher = function(methodName) {
+ var builtInMatcher = jasmine.Matchers.prototype[methodName];
+
+ jasmine.JQuery.matchersClass[methodName] = function() {
+ if (this.actual instanceof jQuery) {
+ var result = jQueryMatchers[methodName].apply(this, arguments);
+ this.actual = jasmine.JQuery.elementToString(this.actual);
+ return result;
+ }
+
+ if (builtInMatcher) {
+ return builtInMatcher.apply(this, arguments);
+ }
+
+ return false;
+ };
+ };
+
+ for(var methodName in jQueryMatchers) {
+ bindMatcher(methodName);
+ }
+})();
+
+beforeEach(function() {
+ this.addMatchers(jasmine.JQuery.matchersClass);
+ this.addMatchers({
+ toHaveBeenTriggeredOn: function(selector) {
+ this.message = function() {
+ return [
+ "Expected event " + this.actual + " to have been triggered on" + selector,
+ "Expected event " + this.actual + " not to have been triggered on" + selector
+ ];
+ };
+ return jasmine.JQuery.events.wasTriggered(selector, this.actual);
+ }
+ })
+});
+
+afterEach(function() {
+ jasmine.getFixtures().cleanUp();
+ jasmine.JQuery.events.cleanUp();
+});
diff --git a/webapp/webapp/static/jasmine-latest/MIT.LICENSE b/webapp/webapp/static/jasmine-latest/MIT.LICENSE
new file mode 100644
index 0000000..7c435ba
--- /dev/null
+++ b/webapp/webapp/static/jasmine-latest/MIT.LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2008-2011 Pivotal Labs
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/webapp/webapp/static/jasmine-latest/jasmine-html.js b/webapp/webapp/static/jasmine-latest/jasmine-html.js
new file mode 100644
index 0000000..7383401
--- /dev/null
+++ b/webapp/webapp/static/jasmine-latest/jasmine-html.js
@@ -0,0 +1,190 @@
+jasmine.TrivialReporter = function(doc) {
+ this.document = doc || document;
+ this.suiteDivs = {};
+ this.logRunningSpecs = false;
+};
+
+jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) {
+ var el = document.createElement(type);
+
+ for (var i = 2; i < arguments.length; i++) {
+ var child = arguments[i];
+
+ if (typeof child === 'string') {
+ el.appendChild(document.createTextNode(child));
+ } else {
+ if (child) { el.appendChild(child); }
+ }
+ }
+
+ for (var attr in attrs) {
+ if (attr == "className") {
+ el[attr] = attrs[attr];
+ } else {
+ el.setAttribute(attr, attrs[attr]);
+ }
+ }
+
+ return el;
+};
+
+jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) {
+ var showPassed, showSkipped;
+
+ this.outerDiv = this.createDom('div', { className: 'jasmine_reporter' },
+ this.createDom('div', { className: 'banner' },
+ this.createDom('div', { className: 'logo' },
+ this.createDom('span', { className: 'title' }, "Jasmine"),
+ this.createDom('span', { className: 'version' }, runner.env.versionString())),
+ this.createDom('div', { className: 'options' },
+ "Show ",
+ showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }),
+ this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "),
+ showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }),
+ this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped")
+ )
+ ),
+
+ this.runnerDiv = this.createDom('div', { className: 'runner running' },
+ this.createDom('a', { className: 'run_spec', href: '?' }, "run all"),
+ this.runnerMessageSpan = this.createDom('span', {}, "Running..."),
+ this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, ""))
+ );
+
+ this.document.body.appendChild(this.outerDiv);
+
+ var suites = runner.suites();
+ for (var i = 0; i < suites.length; i++) {
+ var suite = suites[i];
+ var suiteDiv = this.createDom('div', { className: 'suite' },
+ this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"),
+ this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description));
+ this.suiteDivs[suite.id] = suiteDiv;
+ var parentDiv = this.outerDiv;
+ if (suite.parentSuite) {
+ parentDiv = this.suiteDivs[suite.parentSuite.id];
+ }
+ parentDiv.appendChild(suiteDiv);
+ }
+
+ this.startedAt = new Date();
+
+ var self = this;
+ showPassed.onclick = function(evt) {
+ if (showPassed.checked) {
+ self.outerDiv.className += ' show-passed';
+ } else {
+ self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, '');
+ }
+ };
+
+ showSkipped.onclick = function(evt) {
+ if (showSkipped.checked) {
+ self.outerDiv.className += ' show-skipped';
+ } else {
+ self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, '');
+ }
+ };
+};
+
+jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) {
+ var results = runner.results();
+ var className = (results.failedCount > 0) ? "runner failed" : "runner passed";
+ this.runnerDiv.setAttribute("class", className);
+ //do it twice for IE
+ this.runnerDiv.setAttribute("className", className);
+ var specs = runner.specs();
+ var specCount = 0;
+ for (var i = 0; i < specs.length; i++) {
+ if (this.specFilter(specs[i])) {
+ specCount++;
+ }
+ }
+ var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s");
+ message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s";
+ this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild);
+
+ this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString()));
+};
+
+jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) {
+ var results = suite.results();
+ var status = results.passed() ? 'passed' : 'failed';
+ if (results.totalCount === 0) { // todo: change this to check results.skipped
+ status = 'skipped';
+ }
+ this.suiteDivs[suite.id].className += " " + status;
+};
+
+jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) {
+ if (this.logRunningSpecs) {
+ this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...');
+ }
+};
+
+jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) {
+ var results = spec.results();
+ var status = results.passed() ? 'passed' : 'failed';
+ if (results.skipped) {
+ status = 'skipped';
+ }
+ var specDiv = this.createDom('div', { className: 'spec ' + status },
+ this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"),
+ this.createDom('a', {
+ className: 'description',
+ href: '?spec=' + encodeURIComponent(spec.getFullName()),
+ title: spec.getFullName()
+ }, spec.description));
+
+
+ var resultItems = results.getItems();
+ var messagesDiv = this.createDom('div', { className: 'messages' });
+ for (var i = 0; i < resultItems.length; i++) {
+ var result = resultItems[i];
+
+ if (result.type == 'log') {
+ messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString()));
+ } else if (result.type == 'expect' && result.passed && !result.passed()) {
+ messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message));
+
+ if (result.trace.stack) {
+ messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack));
+ }
+ }
+ }
+
+ if (messagesDiv.childNodes.length > 0) {
+ specDiv.appendChild(messagesDiv);
+ }
+
+ this.suiteDivs[spec.suite.id].appendChild(specDiv);
+};
+
+jasmine.TrivialReporter.prototype.log = function() {
+ var console = jasmine.getGlobal().console;
+ if (console && console.log) {
+ if (console.log.apply) {
+ console.log.apply(console, arguments);
+ } else {
+ console.log(arguments); // ie fix: console.log.apply doesn't exist on ie
+ }
+ }
+};
+
+jasmine.TrivialReporter.prototype.getLocation = function() {
+ return this.document.location;
+};
+
+jasmine.TrivialReporter.prototype.specFilter = function(spec) {
+ var paramMap = {};
+ var params = this.getLocation().search.substring(1).split('&');
+ for (var i = 0; i < params.length; i++) {
+ var p = params[i].split('=');
+ paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
+ }
+
+ if (!paramMap.spec) {
+ return true;
+ }
+ return spec.getFullName().indexOf(paramMap.spec) === 0;
+};
diff --git a/webapp/webapp/static/jasmine-latest/jasmine.css b/webapp/webapp/static/jasmine-latest/jasmine.css
new file mode 100644
index 0000000..6583fe7
--- /dev/null
+++ b/webapp/webapp/static/jasmine-latest/jasmine.css
@@ -0,0 +1,166 @@
+body {
+ font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif;
+}
+
+
+.jasmine_reporter a:visited, .jasmine_reporter a {
+ color: #303;
+}
+
+.jasmine_reporter a:hover, .jasmine_reporter a:active {
+ color: blue;
+}
+
+.run_spec {
+ float:right;
+ padding-right: 5px;
+ font-size: .8em;
+ text-decoration: none;
+}
+
+.jasmine_reporter {
+ margin: 0 5px;
+}
+
+.banner {
+ color: #303;
+ background-color: #fef;
+ padding: 5px;
+}
+
+.logo {
+ float: left;
+ font-size: 1.1em;
+ padding-left: 5px;
+}
+
+.logo .version {
+ font-size: .6em;
+ padding-left: 1em;
+}
+
+.runner.running {
+ background-color: yellow;
+}
+
+
+.options {
+ text-align: right;
+ font-size: .8em;
+}
+
+
+
+
+.suite {
+ border: 1px outset gray;
+ margin: 5px 0;
+ padding-left: 1em;
+}
+
+.suite .suite {
+ margin: 5px;
+}
+
+.suite.passed {
+ background-color: #dfd;
+}
+
+.suite.failed {
+ background-color: #fdd;
+}
+
+.spec {
+ margin: 5px;
+ padding-left: 1em;
+ clear: both;
+}
+
+.spec.failed, .spec.passed, .spec.skipped {
+ padding-bottom: 5px;
+ border: 1px solid gray;
+}
+
+.spec.failed {
+ background-color: #fbb;
+ border-color: red;
+}
+
+.spec.passed {
+ background-color: #bfb;
+ border-color: green;
+}
+
+.spec.skipped {
+ background-color: #bbb;
+}
+
+.messages {
+ border-left: 1px dashed gray;
+ padding-left: 1em;
+ padding-right: 1em;
+}
+
+.passed {
+ background-color: #cfc;
+ display: none;
+}
+
+.failed {
+ background-color: #fbb;
+}
+
+.skipped {
+ color: #777;
+ background-color: #eee;
+ display: none;
+}
+
+
+/*.resultMessage {*/
+ /*white-space: pre;*/
+/*}*/
+
+.resultMessage span.result {
+ display: block;
+ line-height: 2em;
+ color: black;
+}
+
+.resultMessage .mismatch {
+ color: black;
+}
+
+.stackTrace {
+ white-space: pre;
+ font-size: .8em;
+ margin-left: 10px;
+ max-height: 5em;
+ overflow: auto;
+ border: 1px inset red;
+ padding: 1em;
+ background: #eef;
+}
+
+.finished-at {
+ padding-left: 1em;
+ font-size: .6em;
+}
+
+.show-passed .passed,
+.show-skipped .skipped {
+ display: block;
+}
+
+
+#jasmine_content {
+ position:fixed;
+ right: 100%;
+}
+
+.runner {
+ border: 1px solid gray;
+ display: block;
+ margin: 5px 0;
+ padding: 2px 0 2px 10px;
+}
diff --git a/webapp/webapp/static/jasmine-latest/jasmine.js b/webapp/webapp/static/jasmine-latest/jasmine.js
new file mode 100644
index 0000000..922a417
--- /dev/null
+++ b/webapp/webapp/static/jasmine-latest/jasmine.js
@@ -0,0 +1,2476 @@
+var isCommonJS = typeof window == "undefined";
+
+/**
+ * Top level namespace for Jasmine, a lightweight JavaScript BDD/spec/testing framework.
+ *
+ * @namespace
+ */
+var jasmine = {};
+if (isCommonJS) exports.jasmine = jasmine;
+/**
+ * @private
+ */
+jasmine.unimplementedMethod_ = function() {
+ throw new Error("unimplemented method");
+};
+
+/**
+ * Use <code>jasmine.undefined</code> instead of <code>undefined</code>, since <code>undefined</code> is just
+ * a plain old variable and may be redefined by somebody else.
+ *
+ * @private
+ */
+jasmine.undefined = jasmine.___undefined___;
+
+/**
+ * Show diagnostic messages in the console if set to true
+ *
+ */
+jasmine.VERBOSE = false;
+
+/**
+ * Default interval in milliseconds for event loop yields (e.g. to allow network activity or to refresh the screen with the HTML-based runner). Small values here may result in slow test running. Zero means no updates until all tests have completed.
+ *
+ */
+jasmine.DEFAULT_UPDATE_INTERVAL = 250;
+
+/**
+ * Default timeout interval in milliseconds for waitsFor() blocks.
+ */
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000;
+
+jasmine.getGlobal = function() {
+ function getGlobal() {
+ return this;
+ }
+
+ return getGlobal();
+};
+
+/**
+ * Allows for bound functions to be compared. Internal use only.
+ *
+ * @ignore
+ * @private
+ * @param base {Object} bound 'this' for the function
+ * @param name {Function} function to find
+ */
+jasmine.bindOriginal_ = function(base, name) {
+ var original = base[name];
+ if (original.apply) {
+ return function() {
+ return original.apply(base, arguments);
+ };
+ } else {
+ // IE support
+ return jasmine.getGlobal()[name];
+ }
+};
+
+jasmine.setTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'setTimeout');
+jasmine.clearTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearTimeout');
+jasmine.setInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'setInterval');
+jasmine.clearInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearInterval');
+
+jasmine.MessageResult = function(values) {
+ this.type = 'log';
+ this.values = values;
+ this.trace = new Error(); // todo: test better
+};
+
+jasmine.MessageResult.prototype.toString = function() {
+ var text = "";
+ for (var i = 0; i < this.values.length; i++) {
+ if (i > 0) text += " ";
+ if (jasmine.isString_(this.values[i])) {
+ text += this.values[i];
+ } else {
+ text += jasmine.pp(this.values[i]);
+ }
+ }
+ return text;
+};
+
+jasmine.ExpectationResult = function(params) {
+ this.type = 'expect';
+ this.matcherName = params.matcherName;
+ this.passed_ = params.passed;
+ this.expected = params.expected;
+ this.actual = params.actual;
+ this.message = this.passed_ ? 'Passed.' : params.message;
+
+ var trace = (params.trace || new Error(this.message));
+ this.trace = this.passed_ ? '' : trace;
+};
+
+jasmine.ExpectationResult.prototype.toString = function () {
+ return this.message;
+};
+
+jasmine.ExpectationResult.prototype.passed = function () {
+ return this.passed_;
+};
+
+/**
+ * Getter for the Jasmine environment. Ensures one gets created
+ */
+jasmine.getEnv = function() {
+ var env = jasmine.currentEnv_ = jasmine.currentEnv_ || new jasmine.Env();
+ return env;
+};
+
+/**
+ * @ignore
+ * @private
+ * @param value
+ * @returns {Boolean}
+ */
+jasmine.isArray_ = function(value) {
+ return jasmine.isA_("Array", value);
+};
+
+/**
+ * @ignore
+ * @private
+ * @param value
+ * @returns {Boolean}
+ */
+jasmine.isString_ = function(value) {
+ return jasmine.isA_("String", value);
+};
+
+/**
+ * @ignore
+ * @private
+ * @param value
+ * @returns {Boolean}
+ */
+jasmine.isNumber_ = function(value) {
+ return jasmine.isA_("Number", value);
+};
+
+/**
+ * @ignore
+ * @private
+ * @param {String} typeName
+ * @param value
+ * @returns {Boolean}
+ */
+jasmine.isA_ = function(typeName, value) {
+ return Object.prototype.toString.apply(value) === '[object ' + typeName + ']';
+};
+
+/**
+ * Pretty printer for expecations. Takes any object and turns it into a human-readable string.
+ *
+ * @param value {Object} an object to be outputted
+ * @returns {String}
+ */
+jasmine.pp = function(value) {
+ var stringPrettyPrinter = new jasmine.StringPrettyPrinter();
+ stringPrettyPrinter.format(value);
+ return stringPrettyPrinter.string;
+};
+
+/**
+ * Returns true if the object is a DOM Node.
+ *
+ * @param {Object} obj object to check
+ * @returns {Boolean}
+ */
+jasmine.isDomNode = function(obj) {
+ return obj.nodeType > 0;
+};
+
+/**
+ * Returns a matchable 'generic' object of the class type. For use in expecations of type when values don't matter.
+ *
+ * @example
+ * // don't care about which function is passed in, as long as it's a function
+ * expect(mySpy).toHaveBeenCalledWith(jasmine.any(Function));
+ *
+ * @param {Class} clazz
+ * @returns matchable object of the type clazz
+ */
+jasmine.any = function(clazz) {
+ return new jasmine.Matchers.Any(clazz);
+};
+
+/**
+ * Jasmine Spies are test doubles that can act as stubs, spies, fakes or when used in an expecation, mocks.
+ *
+ * Spies should be created in test setup, before expectations. They can then be checked, using the standard Jasmine
+ * expectation syntax. Spies can be checked if they were called or not and what the calling params were.
+ *
+ * A Spy has the following fields: wasCalled, callCount, mostRecentCall, and argsForCall (see docs).
+ *
+ * Spies are torn down at the end of every spec.
+ *
+ * Note: Do <b>not</b> call new jasmine.Spy() directly - a spy must be created using spyOn, jasmine.createSpy or jasmine.createSpyObj.
+ *
+ * @example
+ * // a stub
+ * var myStub = jasmine.createSpy('myStub'); // can be used anywhere
+ *
+ * // spy example
+ * var foo = {
+ * not: function(bool) { return !bool; }
+ * }
+ *
+ * // actual foo.not will not be called, execution stops
+ * spyOn(foo, 'not');
+
+ // foo.not spied upon, execution will continue to implementation
+ * spyOn(foo, 'not').andCallThrough();
+ *
+ * // fake example
+ * var foo = {
+ * not: function(bool) { return !bool; }
+ * }
+ *
+ * // foo.not(val) will return val
+ * spyOn(foo, 'not').andCallFake(function(value) {return value;});
+ *
+ * // mock example
+ * foo.not(7 == 7);
+ * expect(foo.not).toHaveBeenCalled();
+ * expect(foo.not).toHaveBeenCalledWith(true);
+ *
+ * @constructor
+ * @see spyOn, jasmine.createSpy, jasmine.createSpyObj
+ * @param {String} name
+ */
+jasmine.Spy = function(name) {
+ /**
+ * The name of the spy, if provided.
+ */
+ this.identity = name || 'unknown';
+ /**
+ * Is this Object a spy?
+ */
+ this.isSpy = true;
+ /**
+ * The actual function this spy stubs.
+ */
+ this.plan = function() {
+ };
+ /**
+ * Tracking of the most recent call to the spy.
+ * @example
+ * var mySpy = jasmine.createSpy('foo');
+ * mySpy(1, 2);
+ * mySpy.mostRecentCall.args = [1, 2];
+ */
+ this.mostRecentCall = {};
+
+ /**
+ * Holds arguments for each call to the spy, indexed by call count
+ * @example
+ * var mySpy = jasmine.createSpy('foo');
+ * mySpy(1, 2);
+ * mySpy(7, 8);
+ * mySpy.mostRecentCall.args = [7, 8];
+ * mySpy.argsForCall[0] = [1, 2];
+ * mySpy.argsForCall[1] = [7, 8];
+ */
+ this.argsForCall = [];
+ this.calls = [];
+};
+
+/**
+ * Tells a spy to call through to the actual implemenatation.
+ *
+ * @example
+ * var foo = {
+ * bar: function() { // do some stuff }
+ * }
+ *
+ * // defining a spy on an existing property: foo.bar
+ * spyOn(foo, 'bar').andCallThrough();
+ */
+jasmine.Spy.prototype.andCallThrough = function() {
+ this.plan = this.originalValue;
+ return this;
+};
+
+/**
+ * For setting the return value of a spy.
+ *
+ * @example
+ * // defining a spy from scratch: foo() returns 'baz'
+ * var foo = jasmine.createSpy('spy on foo').andReturn('baz');
+ *
+ * // defining a spy on an existing property: foo.bar() returns 'baz'
+ * spyOn(foo, 'bar').andReturn('baz');
+ *
+ * @param {Object} value
+ */
+jasmine.Spy.prototype.andReturn = function(value) {
+ this.plan = function() {
+ return value;
+ };
+ return this;
+};
+
+/**
+ * For throwing an exception when a spy is called.
+ *
+ * @example
+ * // defining a spy from scratch: foo() throws an exception w/ message 'ouch'
+ * var foo = jasmine.createSpy('spy on foo').andThrow('baz');
+ *
+ * // defining a spy on an existing property: foo.bar() throws an exception w/ message 'ouch'
+ * spyOn(foo, 'bar').andThrow('baz');
+ *
+ * @param {String} exceptionMsg
+ */
+jasmine.Spy.prototype.andThrow = function(exceptionMsg) {
+ this.plan = function() {
+ throw exceptionMsg;
+ };
+ return this;
+};
+
+/**
+ * Calls an alternate implementation when a spy is called.
+ *
+ * @example
+ * var baz = function() {
+ * // do some stuff, return something
+ * }
+ * // defining a spy from scratch: foo() calls the function baz
+ * var foo = jasmine.createSpy('spy on foo').andCall(baz);
+ *
+ * // defining a spy on an existing property: foo.bar() calls an anonymnous function
+ * spyOn(foo, 'bar').andCall(function() { return 'baz';} );
+ *
+ * @param {Function} fakeFunc
+ */
+jasmine.Spy.prototype.andCallFake = function(fakeFunc) {
+ this.plan = fakeFunc;
+ return this;
+};
+
+/**
+ * Resets all of a spy's the tracking variables so that it can be used again.
+ *
+ * @example
+ * spyOn(foo, 'bar');
+ *
+ * foo.bar();
+ *
+ * expect(foo.bar.callCount).toEqual(1);
+ *
+ * foo.bar.reset();
+ *
+ * expect(foo.bar.callCount).toEqual(0);
+ */
+jasmine.Spy.prototype.reset = function() {
+ this.wasCalled = false;
+ this.callCount = 0;
+ this.argsForCall = [];
+ this.calls = [];
+ this.mostRecentCall = {};
+};
+
+jasmine.createSpy = function(name) {
+
+ var spyObj = function() {
+ spyObj.wasCalled = true;
+ spyObj.callCount++;
+ var args = jasmine.util.argsToArray(arguments);
+ spyObj.mostRecentCall.object = this;
+ spyObj.mostRecentCall.args = args;
+ spyObj.argsForCall.push(args);
+ spyObj.calls.push({object: this, args: args});
+ return spyObj.plan.apply(this, arguments);
+ };
+
+ var spy = new jasmine.Spy(name);
+
+ for (var prop in spy) {
+ spyObj[prop] = spy[prop];
+ }
+
+ spyObj.reset();
+
+ return spyObj;
+};
+
+/**
+ * Determines whether an object is a spy.
+ *
+ * @param {jasmine.Spy|Object} putativeSpy
+ * @returns {Boolean}
+ */
+jasmine.isSpy = function(putativeSpy) {
+ return putativeSpy && putativeSpy.isSpy;
+};
+
+/**
+ * Creates a more complicated spy: an Object that has every property a function that is a spy. Used for stubbing something
+ * large in one call.
+ *
+ * @param {String} baseName name of spy class
+ * @param {Array} methodNames array of names of methods to make spies
+ */
+jasmine.createSpyObj = function(baseName, methodNames) {
+ if (!jasmine.isArray_(methodNames) || methodNames.length === 0) {
+ throw new Error('createSpyObj requires a non-empty array of method names to create spies for');
+ }
+ var obj = {};
+ for (var i = 0; i < methodNames.length; i++) {
+ obj[methodNames[i]] = jasmine.createSpy(baseName + '.' + methodNames[i]);
+ }
+ return obj;
+};
+
+/**
+ * All parameters are pretty-printed and concatenated together, then written to the current spec's output.
+ *
+ * Be careful not to leave calls to <code>jasmine.log</code> in production code.
+ */
+jasmine.log = function() {
+ var spec = jasmine.getEnv().currentSpec;
+ spec.log.apply(spec, arguments);
+};
+
+/**
+ * Function that installs a spy on an existing object's method name. Used within a Spec to create a spy.
+ *
+ * @example
+ * // spy example
+ * var foo = {
+ * not: function(bool) { return !bool; }
+ * }
+ * spyOn(foo, 'not'); // actual foo.not will not be called, execution stops
+ *
+ * @see jasmine.createSpy
+ * @param obj
+ * @param methodName
+ * @returns a Jasmine spy that can be chained with all spy methods
+ */
+var spyOn = function(obj, methodName) {
+ return jasmine.getEnv().currentSpec.spyOn(obj, methodName);
+};
+if (isCommonJS) exports.spyOn = spyOn;
+
+/**
+ * Creates a Jasmine spec that will be added to the current suite.
+ *
+ * // TODO: pending tests
+ *
+ * @example
+ * it('should be true', function() {
+ * expect(true).toEqual(true);
+ * });
+ *
+ * @param {String} desc description of this specification
+ * @param {Function} func defines the preconditions and expectations of the spec
+ */
+var it = function(desc, func) {
+ return jasmine.getEnv().it(desc, func);
+};
+if (isCommonJS) exports.it = it;
+
+/**
+ * Creates a <em>disabled</em> Jasmine spec.
+ *
+ * A convenience method that allows existing specs to be disabled temporarily during development.
+ *
+ * @param {String} desc description of this specification
+ * @param {Function} func defines the preconditions and expectations of the spec
+ */
+var xit = function(desc, func) {
+ return jasmine.getEnv().xit(desc, func);
+};
+if (isCommonJS) exports.xit = xit;
+
+/**
+ * Starts a chain for a Jasmine expectation.
+ *
+ * It is passed an Object that is the actual value and should chain to one of the many
+ * jasmine.Matchers functions.
+ *
+ * @param {Object} actual Actual value to test against and expected value
+ */
+var expect = function(actual) {
+ return jasmine.getEnv().currentSpec.expect(actual);
+};
+if (isCommonJS) exports.expect = expect;
+
+/**
+ * Defines part of a jasmine spec. Used in cominbination with waits or waitsFor in asynchrnous specs.
+ *
+ * @param {Function} func Function that defines part of a jasmine spec.
+ */
+var runs = function(func) {
+ jasmine.getEnv().currentSpec.runs(func);
+};
+if (isCommonJS) exports.runs = runs;
+
+/**
+ * Waits a fixed time period before moving to the next block.
+ *
+ * @deprecated Use waitsFor() instead
+ * @param {Number} timeout milliseconds to wait
+ */
+var waits = function(timeout) {
+ jasmine.getEnv().currentSpec.waits(timeout);
+};
+if (isCommonJS) exports.waits = waits;
+
+/**
+ * Waits for the latchFunction to return true before proceeding to the next block.
+ *
+ * @param {Function} latchFunction
+ * @param {String} optional_timeoutMessage
+ * @param {Number} optional_timeout
+ */
+var waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) {
+ jasmine.getEnv().currentSpec.waitsFor.apply(jasmine.getEnv().currentSpec, arguments);
+};
+if (isCommonJS) exports.waitsFor = waitsFor;
+
+/**
+ * A function that is called before each spec in a suite.
+ *
+ * Used for spec setup, including validating assumptions.
+ *
+ * @param {Function} beforeEachFunction
+ */
+var beforeEach = function(beforeEachFunction) {
+ jasmine.getEnv().beforeEach(beforeEachFunction);
+};
+if (isCommonJS) exports.beforeEach = beforeEach;
+
+/**
+ * A function that is called after each spec in a suite.
+ *
+ * Used for restoring any state that is hijacked during spec execution.
+ *
+ * @param {Function} afterEachFunction
+ */
+var afterEach = function(afterEachFunction) {
+ jasmine.getEnv().afterEach(afterEachFunction);
+};
+if (isCommonJS) exports.afterEach = afterEach;
+
+/**
+ * Defines a suite of specifications.
+ *
+ * Stores the description and all defined specs in the Jasmine environment as one suite of specs. Variables declared
+ * are accessible by calls to beforeEach, it, and afterEach. Describe blocks can be nested, allowing for specialization
+ * of setup in some tests.
+ *
+ * @example
+ * // TODO: a simple suite
+ *
+ * // TODO: a simple suite with a nested describe block
+ *
+ * @param {String} description A string, usually the class under test.
+ * @param {Function} specDefinitions function that defines several specs.
+ */
+var describe = function(description, specDefinitions) {
+ return jasmine.getEnv().describe(description, specDefinitions);
+};
+if (isCommonJS) exports.describe = describe;
+
+/**
+ * Disables a suite of specifications. Used to disable some suites in a file, or files, temporarily during development.
+ *
+ * @param {String} description A string, usually the class under test.
+ * @param {Function} specDefinitions function that defines several specs.
+ */
+var xdescribe = function(description, specDefinitions) {
+ return jasmine.getEnv().xdescribe(description, specDefinitions);
+};
+if (isCommonJS) exports.xdescribe = xdescribe;
+
+
+// Provide the XMLHttpRequest class for IE 5.x-6.x:
+jasmine.XmlHttpRequest = (typeof XMLHttpRequest == "undefined") ? function() {
+ function tryIt(f) {
+ try {
+ return f();
+ } catch(e) {
+ }
+ return null;
+ }
+
+ var xhr = tryIt(function() {
+ return new ActiveXObject("Msxml2.XMLHTTP.6.0");
+ }) ||
+ tryIt(function() {
+ return new ActiveXObject("Msxml2.XMLHTTP.3.0");
+ }) ||
+ tryIt(function() {
+ return new ActiveXObject("Msxml2.XMLHTTP");
+ }) ||
+ tryIt(function() {
+ return new ActiveXObject("Microsoft.XMLHTTP");
+ });
+
+ if (!xhr) throw new Error("This browser does not support XMLHttpRequest.");
+
+ return xhr;
+} : XMLHttpRequest;
+/**
+ * @namespace
+ */
+jasmine.util = {};
+
+/**
+ * Declare that a child class inherit it's prototype from the parent class.
+ *
+ * @private
+ * @param {Function} childClass
+ * @param {Function} parentClass
+ */
+jasmine.util.inherit = function(childClass, parentClass) {
+ /**
+ * @private
+ */
+ var subclass = function() {
+ };
+ subclass.prototype = parentClass.prototype;
+ childClass.prototype = new subclass();
+};
+
+jasmine.util.formatException = function(e) {
+ var lineNumber;
+ if (e.line) {
+ lineNumber = e.line;
+ }
+ else if (e.lineNumber) {
+ lineNumber = e.lineNumber;
+ }
+
+ var file;
+
+ if (e.sourceURL) {
+ file = e.sourceURL;
+ }
+ else if (e.fileName) {
+ file = e.fileName;
+ }
+
+ var message = (e.name && e.message) ? (e.name + ': ' + e.message) : e.toString();
+
+ if (file && lineNumber) {
+ message += ' in ' + file + ' (line ' + lineNumber + ')';
+ }
+
+ return message;
+};
+
+jasmine.util.htmlEscape = function(str) {
+ if (!str) return str;
+ return str.replace(/&/g, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;');
+};
+
+jasmine.util.argsToArray = function(args) {
+ var arrayOfArgs = [];
+ for (var i = 0; i < args.length; i++) arrayOfArgs.push(args[i]);
+ return arrayOfArgs;
+};
+
+jasmine.util.extend = function(destination, source) {
+ for (var property in source) destination[property] = source[property];
+ return destination;
+};
+
+/**
+ * Environment for Jasmine
+ *
+ * @constructor
+ */
+jasmine.Env = function() {
+ this.currentSpec = null;
+ this.currentSuite = null;
+ this.currentRunner_ = new jasmine.Runner(this);
+
+ this.reporter = new jasmine.MultiReporter();
+
+ this.updateInterval = jasmine.DEFAULT_UPDATE_INTERVAL;
+ this.defaultTimeoutInterval = jasmine.DEFAULT_TIMEOUT_INTERVAL;
+ this.lastUpdate = 0;
+ this.specFilter = function() {
+ return true;
+ };
+
+ this.nextSpecId_ = 0;
+ this.nextSuiteId_ = 0;
+ this.equalityTesters_ = [];
+
+ // wrap matchers
+ this.matchersClass = function() {
+ jasmine.Matchers.apply(this, arguments);
+ };
+ jasmine.util.inherit(this.matchersClass, jasmine.Matchers);
+
+ jasmine.Matchers.wrapInto_(jasmine.Matchers.prototype, this.matchersClass);
+};
+
+
+jasmine.Env.prototype.setTimeout = jasmine.setTimeout;
+jasmine.Env.prototype.clearTimeout = jasmine.clearTimeout;
+jasmine.Env.prototype.setInterval = jasmine.setInterval;
+jasmine.Env.prototype.clearInterval = jasmine.clearInterval;
+
+/**
+ * @returns an object containing jasmine version build info, if set.
+ */
+jasmine.Env.prototype.version = function () {
+ if (jasmine.version_) {
+ return jasmine.version_;
+ } else {
+ throw new Error('Version not set');
+ }
+};
+
+/**
+ * @returns string containing jasmine version build info, if set.
+ */
+jasmine.Env.prototype.versionString = function() {
+ if (!jasmine.version_) {
+ return "version unknown";
+ }
+
+ var version = this.version();
+ var dotted_version = version.major + "." + version.minor + "." + version.build;
+ if (version.rc) {
+ dotted_version += ".rc" + version.rc;
+ }
+ return dotted_version + " revision " + version.revision;
+};
+
+/**
+ * @returns a sequential integer starting at 0
+ */
+jasmine.Env.prototype.nextSpecId = function () {
+ return this.nextSpecId_++;
+};
+
+/**
+ * @returns a sequential integer starting at 0
+ */
+jasmine.Env.prototype.nextSuiteId = function () {
+ return this.nextSuiteId_++;
+};
+
+/**
+ * Register a reporter to receive status updates from Jasmine.
+ * @param {jasmine.Reporter} reporter An object which will receive status updates.
+ */
+jasmine.Env.prototype.addReporter = function(reporter) {
+ this.reporter.addReporter(reporter);
+};
+
+jasmine.Env.prototype.execute = function() {
+ this.currentRunner_.execute();
+};
+
+jasmine.Env.prototype.describe = function(description, specDefinitions) {
+ var suite = new jasmine.Suite(this, description, specDefinitions, this.currentSuite);
+
+ var parentSuite = this.currentSuite;
+ if (parentSuite) {
+ parentSuite.add(suite);
+ } else {
+ this.currentRunner_.add(suite);
+ }
+
+ this.currentSuite = suite;
+
+ var declarationError = null;
+ try {
+ specDefinitions.call(suite);
+ } catch(e) {
+ declarationError = e;
+ }
+
+ if (declarationError) {
+ this.it("encountered a declaration exception", function() {
+ throw declarationError;
+ });
+ }
+
+ this.currentSuite = parentSuite;
+
+ return suite;
+};
+
+jasmine.Env.prototype.beforeEach = function(beforeEachFunction) {
+ if (this.currentSuite) {
+ this.currentSuite.beforeEach(beforeEachFunction);
+ } else {
+ this.currentRunner_.beforeEach(beforeEachFunction);
+ }
+};
+
+jasmine.Env.prototype.currentRunner = function () {
+ return this.currentRunner_;
+};
+
+jasmine.Env.prototype.afterEach = function(afterEachFunction) {
+ if (this.currentSuite) {
+ this.currentSuite.afterEach(afterEachFunction);
+ } else {
+ this.currentRunner_.afterEach(afterEachFunction);
+ }
+
+};
+
+jasmine.Env.prototype.xdescribe = function(desc, specDefinitions) {
+ return {
+ execute: function() {
+ }
+ };
+};
+
+jasmine.Env.prototype.it = function(description, func) {
+ var spec = new jasmine.Spec(this, this.currentSuite, description);
+ this.currentSuite.add(spec);
+ this.currentSpec = spec;
+
+ if (func) {
+ spec.runs(func);
+ }
+
+ return spec;
+};
+
+jasmine.Env.prototype.xit = function(desc, func) {
+ return {
+ id: this.nextSpecId(),
+ runs: function() {
+ }
+ };
+};
+
+jasmine.Env.prototype.compareObjects_ = function(a, b, mismatchKeys, mismatchValues) {
+ if (a.__Jasmine_been_here_before__ === b && b.__Jasmine_been_here_before__ === a) {
+ return true;
+ }
+
+ a.__Jasmine_been_here_before__ = b;
+ b.__Jasmine_been_here_before__ = a;
+
+ var hasKey = function(obj, keyName) {
+ return obj !== null && obj[keyName] !== jasmine.undefined;
+ };
+
+ for (var property in b) {
+ if (!hasKey(a, property) && hasKey(b, property)) {
+ mismatchKeys.push("expected has key '" + property + "', but missing from actual.");
+ }
+ }
+ for (property in a) {
+ if (!hasKey(b, property) && hasKey(a, property)) {
+ mismatchKeys.push("expected missing key '" + property + "', but present in actual.");
+ }
+ }
+ for (property in b) {
+ if (property == '__Jasmine_been_here_before__') continue;
+ if (!this.equals_(a[property], b[property], mismatchKeys, mismatchValues)) {
+ mismatchValues.push("'" + property + "' was '" + (b[property] ? jasmine.util.htmlEscape(b[property].toString()) : b[property]) + "' in expected, but was '" + (a[property] ? jasmine.util.htmlEscape(a[property].toString()) : a[property]) + "' in actual.");
+ }
+ }
+
+ if (jasmine.isArray_(a) && jasmine.isArray_(b) && a.length != b.length) {
+ mismatchValues.push("arrays were not the same length");
+ }
+
+ delete a.__Jasmine_been_here_before__;
+ delete b.__Jasmine_been_here_before__;
+ return (mismatchKeys.length === 0 && mismatchValues.length === 0);
+};
+
+jasmine.Env.prototype.equals_ = function(a, b, mismatchKeys, mismatchValues) {
+ mismatchKeys = mismatchKeys || [];
+ mismatchValues = mismatchValues || [];
+
+ for (var i = 0; i < this.equalityTesters_.length; i++) {
+ var equalityTester = this.equalityTesters_[i];
+ var result = equalityTester(a, b, this, mismatchKeys, mismatchValues);
+ if (result !== jasmine.undefined) return result;
+ }
+
+ if (a === b) return true;
+
+ if (a === jasmine.undefined || a === null || b === jasmine.undefined || b === null) {
+ return (a == jasmine.undefined && b == jasmine.undefined);
+ }
+
+ if (jasmine.isDomNode(a) && jasmine.isDomNode(b)) {
+ return a === b;
+ }
+
+ if (a instanceof Date && b instanceof Date) {
+ return a.getTime() == b.getTime();
+ }
+
+ if (a instanceof jasmine.Matchers.Any) {
+ return a.matches(b);
+ }
+
+ if (b instanceof jasmine.Matchers.Any) {
+ return b.matches(a);
+ }
+
+ if (jasmine.isString_(a) && jasmine.isString_(b)) {
+ return (a == b);
+ }
+
+ if (jasmine.isNumber_(a) && jasmine.isNumber_(b)) {
+ return (a == b);
+ }
+
+ if (typeof a === "object" && typeof b === "object") {
+ return this.compareObjects_(a, b, mismatchKeys, mismatchValues);
+ }
+
+ //Straight check
+ return (a === b);
+};
+
+jasmine.Env.prototype.contains_ = function(haystack, needle) {
+ if (jasmine.isArray_(haystack)) {
+ for (var i = 0; i < haystack.length; i++) {
+ if (this.equals_(haystack[i], needle)) return true;
+ }
+ return false;
+ }
+ return haystack.indexOf(needle) >= 0;
+};
+
+jasmine.Env.prototype.addEqualityTester = function(equalityTester) {
+ this.equalityTesters_.push(equalityTester);
+};
+/** No-op base class for Jasmine reporters.
+ *
+ * @constructor
+ */
+jasmine.Reporter = function() {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportRunnerStarting = function(runner) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportRunnerResults = function(runner) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportSuiteResults = function(suite) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportSpecStarting = function(spec) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.reportSpecResults = function(spec) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.Reporter.prototype.log = function(str) {
+};
+
+/**
+ * Blocks are functions with executable code that make up a spec.
+ *
+ * @constructor
+ * @param {jasmine.Env} env
+ * @param {Function} func
+ * @param {jasmine.Spec} spec
+ */
+jasmine.Block = function(env, func, spec) {
+ this.env = env;
+ this.func = func;
+ this.spec = spec;
+};
+
+jasmine.Block.prototype.execute = function(onComplete) {
+ try {
+ this.func.apply(this.spec);
+ } catch (e) {
+ this.spec.fail(e);
+ }
+ onComplete();
+};
+/** JavaScript API reporter.
+ *
+ * @constructor
+ */
+jasmine.JsApiReporter = function() {
+ this.started = false;
+ this.finished = false;
+ this.suites_ = [];
+ this.results_ = {};
+};
+
+jasmine.JsApiReporter.prototype.reportRunnerStarting = function(runner) {
+ this.started = true;
+ var suites = runner.topLevelSuites();
+ for (var i = 0; i < suites.length; i++) {
+ var suite = suites[i];
+ this.suites_.push(this.summarize_(suite));
+ }
+};
+
+jasmine.JsApiReporter.prototype.suites = function() {
+ return this.suites_;
+};
+
+jasmine.JsApiReporter.prototype.summarize_ = function(suiteOrSpec) {
+ var isSuite = suiteOrSpec instanceof jasmine.Suite;
+ var summary = {
+ id: suiteOrSpec.id,
+ name: suiteOrSpec.description,
+ type: isSuite ? 'suite' : 'spec',
+ children: []
+ };
+
+ if (isSuite) {
+ var children = suiteOrSpec.children();
+ for (var i = 0; i < children.length; i++) {
+ summary.children.push(this.summarize_(children[i]));
+ }
+ }
+ return summary;
+};
+
+jasmine.JsApiReporter.prototype.results = function() {
+ return this.results_;
+};
+
+jasmine.JsApiReporter.prototype.resultsForSpec = function(specId) {
+ return this.results_[specId];
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.JsApiReporter.prototype.reportRunnerResults = function(runner) {
+ this.finished = true;
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.JsApiReporter.prototype.reportSuiteResults = function(suite) {
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.JsApiReporter.prototype.reportSpecResults = function(spec) {
+ this.results_[spec.id] = {
+ messages: spec.results().getItems(),
+ result: spec.results().failedCount > 0 ? "failed" : "passed"
+ };
+};
+
+//noinspection JSUnusedLocalSymbols
+jasmine.JsApiReporter.prototype.log = function(str) {
+};
+
+jasmine.JsApiReporter.prototype.resultsForSpecs = function(specIds){
+ var results = {};
+ for (var i = 0; i < specIds.length; i++) {
+ var specId = specIds[i];
+ results[specId] = this.summarizeResult_(this.results_[specId]);
+ }
+ return results;
+};
+
+jasmine.JsApiReporter.prototype.summarizeResult_ = function(result){
+ var summaryMessages = [];
+ var messagesLength = result.messages.length;
+ for (var messageIndex = 0; messageIndex < messagesLength; messageIndex++) {
+ var resultMessage = result.messages[messageIndex];
+ summaryMessages.push({
+ text: resultMessage.type == 'log' ? resultMessage.toString() : jasmine.undefined,
+ passed: resultMessage.passed ? resultMessage.passed() : true,
+ type: resultMessage.type,
+ message: resultMessage.message,
+ trace: {
+ stack: resultMessage.passed && !resultMessage.passed() ? resultMessage.trace.stack : jasmine.undefined
+ }
+ });
+ }
+
+ return {
+ result : result.result,
+ messages : summaryMessages
+ };
+};
+
+/**
+ * @constructor
+ * @param {jasmine.Env} env
+ * @param actual
+ * @param {jasmine.Spec} spec
+ */
+jasmine.Matchers = function(env, actual, spec, opt_isNot) {
+ this.env = env;
+ this.actual = actual;
+ this.spec = spec;
+ this.isNot = opt_isNot || false;
+ this.reportWasCalled_ = false;
+};
+
+// todo: @deprecated as of Jasmine 0.11, remove soon [xw]
+jasmine.Matchers.pp = function(str) {
+ throw new Error("jasmine.Matchers.pp() is no longer supported, please use jasmine.pp() instead!");
+};
+
+// todo: @deprecated Deprecated as of Jasmine 0.10. Rewrite your custom matchers to return true or false. [xw]
+jasmine.Matchers.prototype.report = function(result, failing_message, details) {
+ throw new Error("As of jasmine 0.11, custom matchers must be implemented differently -- please see jasmine docs");
+};
+
+jasmine.Matchers.wrapInto_ = function(prototype, matchersClass) {
+ for (var methodName in prototype) {
+ if (methodName == 'report') continue;
+ var orig = prototype[methodName];
+ matchersClass.prototype[methodName] = jasmine.Matchers.matcherFn_(methodName, orig);
+ }
+};
+
+jasmine.Matchers.matcherFn_ = function(matcherName, matcherFunction) {
+ return function() {
+ var matcherArgs = jasmine.util.argsToArray(arguments);
+ var result = matcherFunction.apply(this, arguments);
+
+ if (this.isNot) {
+ result = !result;
+ }
+
+ if (this.reportWasCalled_) return result;
+
+ var message;
+ if (!result) {
+ if (this.message) {
+ message = this.message.apply(this, arguments);
+ if (jasmine.isArray_(message)) {
+ message = message[this.isNot ? 1 : 0];
+ }
+ } else {
+ var englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); });
+ message = "Expected " + jasmine.pp(this.actual) + (this.isNot ? " not " : " ") + englishyPredicate;
+ if (matcherArgs.length > 0) {
+ for (var i = 0; i < matcherArgs.length; i++) {
+ if (i > 0) message += ",";
+ message += " " + jasmine.pp(matcherArgs[i]);
+ }
+ }
+ message += ".";
+ }
+ }
+ var expectationResult = new jasmine.ExpectationResult({
+ matcherName: matcherName,
+ passed: result,
+ expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0],
+ actual: this.actual,
+ message: message
+ });
+ this.spec.addMatcherResult(expectationResult);
+ return jasmine.undefined;
+ };
+};
+
+
+
+
+/**
+ * toBe: compares the actual to the expected using ===
+ * @param expected
+ */
+jasmine.Matchers.prototype.toBe = function(expected) {
+ return this.actual === expected;
+};
+
+/**
+ * toNotBe: compares the actual to the expected using !==
+ * @param expected
+ * @deprecated as of 1.0. Use not.toBe() instead.
+ */
+jasmine.Matchers.prototype.toNotBe = function(expected) {
+ return this.actual !== expected;
+};
+
+/**
+ * toEqual: compares the actual to the expected using common sense equality. Handles Objects, Arrays, etc.
+ *
+ * @param expected
+ */
+jasmine.Matchers.prototype.toEqual = function(expected) {
+ return this.env.equals_(this.actual, expected);
+};
+
+/**
+ * toNotEqual: compares the actual to the expected using the ! of jasmine.Matchers.toEqual
+ * @param expected
+ * @deprecated as of 1.0. Use not.toNotEqual() instead.
+ */
+jasmine.Matchers.prototype.toNotEqual = function(expected) {
+ return !this.env.equals_(this.actual, expected);
+};
+
+/**
+ * Matcher that compares the actual to the expected using a regular expression. Constructs a RegExp, so takes
+ * a pattern or a String.
+ *
+ * @param expected
+ */
+jasmine.Matchers.prototype.toMatch = function(expected) {
+ return new RegExp(expected).test(this.actual);
+};
+
+/**
+ * Matcher that compares the actual to the expected using the boolean inverse of jasmine.Matchers.toMatch
+ * @param expected
+ * @deprecated as of 1.0. Use not.toMatch() instead.
+ */
+jasmine.Matchers.prototype.toNotMatch = function(expected) {
+ return !(new RegExp(expected).test(this.actual));
+};
+
+/**
+ * Matcher that compares the actual to jasmine.undefined.
+ */
+jasmine.Matchers.prototype.toBeDefined = function() {
+ return (this.actual !== jasmine.undefined);
+};
+
+/**
+ * Matcher that compares the actual to jasmine.undefined.
+ */
+jasmine.Matchers.prototype.toBeUndefined = function() {
+ return (this.actual === jasmine.undefined);
+};
+
+/**
+ * Matcher that compares the actual to null.
+ */
+jasmine.Matchers.prototype.toBeNull = function() {
+ return (this.actual === null);
+};
+
+/**
+ * Matcher that boolean not-nots the actual.
+ */
+jasmine.Matchers.prototype.toBeTruthy = function() {
+ return !!this.actual;
+};
+
+
+/**
+ * Matcher that boolean nots the actual.
+ */
+jasmine.Matchers.prototype.toBeFalsy = function() {
+ return !this.actual;
+};
+
+
+/**
+ * Matcher that checks to see if the actual, a Jasmine spy, was called.
+ */
+jasmine.Matchers.prototype.toHaveBeenCalled = function() {
+ if (arguments.length > 0) {
+ throw new Error('toHaveBeenCalled does not take arguments, use toHaveBeenCalledWith');
+ }
+
+ if (!jasmine.isSpy(this.actual)) {
+ throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');
+ }
+
+ this.message = function() {
+ return [
+ "Expected spy " + this.actual.identity + " to have been called.",
+ "Expected spy " + this.actual.identity + " not to have been called."
+ ];
+ };
+
+ return this.actual.wasCalled;
+};
+
+/** @deprecated Use expect(xxx).toHaveBeenCalled() instead */
+jasmine.Matchers.prototype.wasCalled = jasmine.Matchers.prototype.toHaveBeenCalled;
+
+/**
+ * Matcher that checks to see if the actual, a Jasmine spy, was not called.
+ *
+ * @deprecated Use expect(xxx).not.toHaveBeenCalled() instead
+ */
+jasmine.Matchers.prototype.wasNotCalled = function() {
+ if (arguments.length > 0) {
+ throw new Error('wasNotCalled does not take arguments');
+ }
+
+ if (!jasmine.isSpy(this.actual)) {
+ throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');
+ }
+
+ this.message = function() {
+ return [
+ "Expected spy " + this.actual.identity + " to not have been called.",
+ "Expected spy " + this.actual.identity + " to have been called."
+ ];
+ };
+
+ return !this.actual.wasCalled;
+};
+
+/**
+ * Matcher that checks to see if the actual, a Jasmine spy, was called with a set of parameters.
+ *
+ * @example
+ *
+ */
+jasmine.Matchers.prototype.toHaveBeenCalledWith = function() {
+ var expectedArgs = jasmine.util.argsToArray(arguments);
+ if (!jasmine.isSpy(this.actual)) {
+ throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');
+ }
+ this.message = function() {
+ if (this.actual.callCount === 0) {
+ // todo: what should the failure message for .not.toHaveBeenCalledWith() be? is this right? test better. [xw]
+ return [
+ "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but it was never called.",
+ "Expected spy " + this.actual.identity + " not to have been called with " + jasmine.pp(expectedArgs) + " but it was."
+ ];
+ } else {
+ return [
+ "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but was called with " + jasmine.pp(this.actual.argsForCall),
+ "Expected spy " + this.actual.identity + " not to have been called with " + jasmine.pp(expectedArgs) + " but was called with " + jasmine.pp(this.actual.argsForCall)
+ ];
+ }
+ };
+
+ return this.env.contains_(this.actual.argsForCall, expectedArgs);
+};
+
+/** @deprecated Use expect(xxx).toHaveBeenCalledWith() instead */
+jasmine.Matchers.prototype.wasCalledWith = jasmine.Matchers.prototype.toHaveBeenCalledWith;
+
+/** @deprecated Use expect(xxx).not.toHaveBeenCalledWith() instead */
+jasmine.Matchers.prototype.wasNotCalledWith = function() {
+ var expectedArgs = jasmine.util.argsToArray(arguments);
+ if (!jasmine.isSpy(this.actual)) {
+ throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');
+ }
+
+ this.message = function() {
+ return [
+ "Expected spy not to have been called with " + jasmine.pp(expectedArgs) + " but it was",
+ "Expected spy to have been called with " + jasmine.pp(expectedArgs) + " but it was"
+ ];
+ };
+
+ return !this.env.contains_(this.actual.argsForCall, expectedArgs);
+};
+
+/**
+ * Matcher that checks that the expected item is an element in the actual Array.
+ *
+ * @param {Object} expected
+ */
+jasmine.Matchers.prototype.toContain = function(expected) {
+ return this.env.contains_(this.actual, expected);
+};
+
+/**
+ * Matcher that checks that the expected item is NOT an element in the actual Array.
+ *
+ * @param {Object} expected
+ * @deprecated as of 1.0. Use not.toNotContain() instead.
+ */
+jasmine.Matchers.prototype.toNotContain = function(expected) {
+ return !this.env.contains_(this.actual, expected);
+};
+
+jasmine.Matchers.prototype.toBeLessThan = function(expected) {
+ return this.actual < expected;
+};
+
+jasmine.Matchers.prototype.toBeGreaterThan = function(expected) {
+ return this.actual > expected;
+};
+
+/**
+ * Matcher that checks that the expected item is equal to the actual item
+ * up to a given level of decimal precision (default 2).
+ *
+ * @param {Number} expected
+ * @param {Number} precision
+ */
+jasmine.Matchers.prototype.toBeCloseTo = function(expected, precision) {
+ if (!(precision === 0)) {
+ precision = precision || 2;
+ }
+ var multiplier = Math.pow(10, precision);
+ var actual = Math.round(this.actual * multiplier);
+ expected = Math.round(expected * multiplier);
+ return expected == actual;
+};
+
+/**
+ * Matcher that checks that the expected exception was thrown by the actual.
+ *
+ * @param {String} expected
+ */
+jasmine.Matchers.prototype.toThrow = function(expected) {
+ var result = false;
+ var exception;
+ if (typeof this.actual != 'function') {
+ throw new Error('Actual is not a function');
+ }
+ try {
+ this.actual();
+ } catch (e) {
+ exception = e;
+ }
+ if (exception) {
+ result = (expected === jasmine.undefined || this.env.equals_(exception.message || exception, expected.message || expected));
+ }
+
+ var not = this.isNot ? "not " : "";
+
+ this.message = function() {
+ if (exception && (expected === jasmine.undefined || !this.env.equals_(exception.message || exception, expected.message || expected))) {
+ return ["Expected function " + not + "to throw", expected ? expected.message || expected : "an exception", ", but it threw", exception.message || exception].join(' ');
+ } else {
+ return "Expected function to throw an exception.";
+ }
+ };
+
+ return result;
+};
+
+jasmine.Matchers.Any = function(expectedClass) {
+ this.expectedClass = expectedClass;
+};
+
+jasmine.Matchers.Any.prototype.matches = function(other) {
+ if (this.expectedClass == String) {
+ return typeof other == 'string' || other instanceof String;
+ }
+
+ if (this.expectedClass == Number) {
+ return typeof other == 'number' || other instanceof Number;
+ }
+
+ if (this.expectedClass == Function) {
+ return typeof other == 'function' || other instanceof Function;
+ }
+
+ if (this.expectedClass == Object) {
+ return typeof other == 'object';
+ }
+
+ return other instanceof this.expectedClass;
+};
+
+jasmine.Matchers.Any.prototype.toString = function() {
+ return '<jasmine.any(' + this.expectedClass + ')>';
+};
+
+/**
+ * @constructor
+ */
+jasmine.MultiReporter = function() {
+ this.subReporters_ = [];
+};
+jasmine.util.inherit(jasmine.MultiReporter, jasmine.Reporter);
+
+jasmine.MultiReporter.prototype.addReporter = function(reporter) {
+ this.subReporters_.push(reporter);
+};
+
+(function() {
+ var functionNames = [
+ "reportRunnerStarting",
+ "reportRunnerResults",
+ "reportSuiteResults",
+ "reportSpecStarting",
+ "reportSpecResults",
+ "log"
+ ];
+ for (var i = 0; i < functionNames.length; i++) {
+ var functionName = functionNames[i];
+ jasmine.MultiReporter.prototype[functionName] = (function(functionName) {
+ return function() {
+ for (var j = 0; j < this.subReporters_.length; j++) {
+ var subReporter = this.subReporters_[j];
+ if (subReporter[functionName]) {
+ subReporter[functionName].apply(subReporter, arguments);
+ }
+ }
+ };
+ })(functionName);
+ }
+})();
+/**
+ * Holds results for a set of Jasmine spec. Allows for the results array to hold another jasmine.NestedResults
+ *
+ * @constructor
+ */
+jasmine.NestedResults = function() {
+ /**
+ * The total count of results
+ */
+ this.totalCount = 0;
+ /**
+ * Number of passed results
+ */
+ this.passedCount = 0;
+ /**
+ * Number of failed results
+ */
+ this.failedCount = 0;
+ /**
+ * Was this suite/spec skipped?
+ */
+ this.skipped = false;
+ /**
+ * @ignore
+ */
+ this.items_ = [];
+};
+
+/**
+ * Roll up the result counts.
+ *
+ * @param result
+ */
+jasmine.NestedResults.prototype.rollupCounts = function(result) {
+ this.totalCount += result.totalCount;
+ this.passedCount += result.passedCount;
+ this.failedCount += result.failedCount;
+};
+
+/**
+ * Adds a log message.
+ * @param values Array of message parts which will be concatenated later.
+ */
+jasmine.NestedResults.prototype.log = function(values) {
+ this.items_.push(new jasmine.MessageResult(values));
+};
+
+/**
+ * Getter for the results: message & results.
+ */
+jasmine.NestedResults.prototype.getItems = function() {
+ return this.items_;
+};
+
+/**
+ * Adds a result, tracking counts (total, passed, & failed)
+ * @param {jasmine.ExpectationResult|jasmine.NestedResults} result
+ */
+jasmine.NestedResults.prototype.addResult = function(result) {
+ if (result.type != 'log') {
+ if (result.items_) {
+ this.rollupCounts(result);
+ } else {
+ this.totalCount++;
+ if (result.passed()) {
+ this.passedCount++;
+ } else {
+ this.failedCount++;
+ }
+ }
+ }
+ this.items_.push(result);
+};
+
+/**
+ * @returns {Boolean} True if <b>everything</b> below passed
+ */
+jasmine.NestedResults.prototype.passed = function() {
+ return this.passedCount === this.totalCount;
+};
+/**
+ * Base class for pretty printing for expectation results.
+ */
+jasmine.PrettyPrinter = function() {
+ this.ppNestLevel_ = 0;
+};
+
+/**
+ * Formats a value in a nice, human-readable string.
+ *
+ * @param value
+ */
+jasmine.PrettyPrinter.prototype.format = function(value) {
+ if (this.ppNestLevel_ > 40) {
+ throw new Error('jasmine.PrettyPrinter: format() nested too deeply!');
+ }
+
+ this.ppNestLevel_++;
+ try {
+ if (value === jasmine.undefined) {
+ this.emitScalar('undefined');
+ } else if (value === null) {
+ this.emitScalar('null');
+ } else if (value === jasmine.getGlobal()) {
+ this.emitScalar('<global>');
+ } else if (value instanceof jasmine.Matchers.Any) {
+ this.emitScalar(value.toString());
+ } else if (typeof value === 'string') {
+ this.emitString(value);
+ } else if (jasmine.isSpy(value)) {
+ this.emitScalar("spy on " + value.identity);
+ } else if (value instanceof RegExp) {
+ this.emitScalar(value.toString());
+ } else if (typeof value === 'function') {
+ this.emitScalar('Function');
+ } else if (typeof value.nodeType === 'number') {
+ this.emitScalar('HTMLNode');
+ } else if (value instanceof Date) {
+ this.emitScalar('Date(' + value + ')');
+ } else if (value.__Jasmine_been_here_before__) {
+ this.emitScalar('<circular reference: ' + (jasmine.isArray_(value) ? 'Array' : 'Object') + '>');
+ } else if (jasmine.isArray_(value) || typeof value == 'object') {
+ value.__Jasmine_been_here_before__ = true;
+ if (jasmine.isArray_(value)) {
+ this.emitArray(value);
+ } else {
+ this.emitObject(value);
+ }
+ delete value.__Jasmine_been_here_before__;
+ } else {
+ this.emitScalar(value.toString());
+ }
+ } finally {
+ this.ppNestLevel_--;
+ }
+};
+
+jasmine.PrettyPrinter.prototype.iterateObject = function(obj, fn) {
+ for (var property in obj) {
+ if (property == '__Jasmine_been_here_before__') continue;
+ fn(property, obj.__lookupGetter__ ? (obj.__lookupGetter__(property) !== jasmine.undefined &&
+ obj.__lookupGetter__(property) !== null) : false);
+ }
+};
+
+jasmine.PrettyPrinter.prototype.emitArray = jasmine.unimplementedMethod_;
+jasmine.PrettyPrinter.prototype.emitObject = jasmine.unimplementedMethod_;
+jasmine.PrettyPrinter.prototype.emitScalar = jasmine.unimplementedMethod_;
+jasmine.PrettyPrinter.prototype.emitString = jasmine.unimplementedMethod_;
+
+jasmine.StringPrettyPrinter = function() {
+ jasmine.PrettyPrinter.call(this);
+
+ this.string = '';
+};
+jasmine.util.inherit(jasmine.StringPrettyPrinter, jasmine.PrettyPrinter);
+
+jasmine.StringPrettyPrinter.prototype.emitScalar = function(value) {
+ this.append(value);
+};
+
+jasmine.StringPrettyPrinter.prototype.emitString = function(value) {
+ this.append("'" + value + "'");
+};
+
+jasmine.StringPrettyPrinter.prototype.emitArray = function(array) {
+ this.append('[ ');
+ for (var i = 0; i < array.length; i++) {
+ if (i > 0) {
+ this.append(', ');
+ }
+ this.format(array[i]);
+ }
+ this.append(' ]');
+};
+
+jasmine.StringPrettyPrinter.prototype.emitObject = function(obj) {
+ var self = this;
+ this.append('{ ');
+ var first = true;
+
+ this.iterateObject(obj, function(property, isGetter) {
+ if (first) {
+ first = false;
+ } else {
+ self.append(', ');
+ }
+
+ self.append(property);
+ self.append(' : ');
+ if (isGetter) {
+ self.append('<getter>');
+ } else {
+ self.format(obj[property]);
+ }
+ });
+
+ this.append(' }');
+};
+
+jasmine.StringPrettyPrinter.prototype.append = function(value) {
+ this.string += value;
+};
+jasmine.Queue = function(env) {
+ this.env = env;
+ this.blocks = [];
+ this.running = false;
+ this.index = 0;
+ this.offset = 0;
+ this.abort = false;
+};
+
+jasmine.Queue.prototype.addBefore = function(block) {
+ this.blocks.unshift(block);
+};
+
+jasmine.Queue.prototype.add = function(block) {
+ this.blocks.push(block);
+};
+
+jasmine.Queue.prototype.insertNext = function(block) {
+ this.blocks.splice((this.index + this.offset + 1), 0, block);
+ this.offset++;
+};
+
+jasmine.Queue.prototype.start = function(onComplete) {
+ this.running = true;
+ this.onComplete = onComplete;
+ this.next_();
+};
+
+jasmine.Queue.prototype.isRunning = function() {
+ return this.running;
+};
+
+jasmine.Queue.LOOP_DONT_RECURSE = true;
+
+jasmine.Queue.prototype.next_ = function() {
+ var self = this;
+ var goAgain = true;
+
+ while (goAgain) {
+ goAgain = false;
+
+ if (self.index < self.blocks.length && !this.abort) {
+ var calledSynchronously = true;
+ var completedSynchronously = false;
+
+ var onComplete = function () {
+ if (jasmine.Queue.LOOP_DONT_RECURSE && calledSynchronously) {
+ completedSynchronously = true;
+ return;
+ }
+
+ if (self.blocks[self.index].abort) {
+ self.abort = true;
+ }
+
+ self.offset = 0;
+ self.index++;
+
+ var now = new Date().getTime();
+ if (self.env.updateInterval && now - self.env.lastUpdate > self.env.updateInterval) {
+ self.env.lastUpdate = now;
+ self.env.setTimeout(function() {
+ self.next_();
+ }, 0);
+ } else {
+ if (jasmine.Queue.LOOP_DONT_RECURSE && completedSynchronously) {
+ goAgain = true;
+ } else {
+ self.next_();
+ }
+ }
+ };
+ self.blocks[self.index].execute(onComplete);
+
+ calledSynchronously = false;
+ if (completedSynchronously) {
+ onComplete();
+ }
+
+ } else {
+ self.running = false;
+ if (self.onComplete) {
+ self.onComplete();
+ }
+ }
+ }
+};
+
+jasmine.Queue.prototype.results = function() {
+ var results = new jasmine.NestedResults();
+ for (var i = 0; i < this.blocks.length; i++) {
+ if (this.blocks[i].results) {
+ results.addResult(this.blocks[i].results());
+ }
+ }
+ return results;
+};
+
+
+/**
+ * Runner
+ *
+ * @constructor
+ * @param {jasmine.Env} env
+ */
+jasmine.Runner = function(env) {
+ var self = this;
+ self.env = env;
+ self.queue = new jasmine.Queue(env);
+ self.before_ = [];
+ self.after_ = [];
+ self.suites_ = [];
+};
+
+jasmine.Runner.prototype.execute = function() {
+ var self = this;
+ if (self.env.reporter.reportRunnerStarting) {
+ self.env.reporter.reportRunnerStarting(this);
+ }
+ self.queue.start(function () {
+ self.finishCallback();
+ });
+};
+
+jasmine.Runner.prototype.beforeEach = function(beforeEachFunction) {
+ beforeEachFunction.typeName = 'beforeEach';
+ this.before_.splice(0,0,beforeEachFunction);
+};
+
+jasmine.Runner.prototype.afterEach = function(afterEachFunction) {
+ afterEachFunction.typeName = 'afterEach';
+ this.after_.splice(0,0,afterEachFunction);
+};
+
+
+jasmine.Runner.prototype.finishCallback = function() {
+ this.env.reporter.reportRunnerResults(this);
+};
+
+jasmine.Runner.prototype.addSuite = function(suite) {
+ this.suites_.push(suite);
+};
+
+jasmine.Runner.prototype.add = function(block) {
+ if (block instanceof jasmine.Suite) {
+ this.addSuite(block);
+ }
+ this.queue.add(block);
+};
+
+jasmine.Runner.prototype.specs = function () {
+ var suites = this.suites();
+ var specs = [];
+ for (var i = 0; i < suites.length; i++) {
+ specs = specs.concat(suites[i].specs());
+ }
+ return specs;
+};
+
+jasmine.Runner.prototype.suites = function() {
+ return this.suites_;
+};
+
+jasmine.Runner.prototype.topLevelSuites = function() {
+ var topLevelSuites = [];
+ for (var i = 0; i < this.suites_.length; i++) {
+ if (!this.suites_[i].parentSuite) {
+ topLevelSuites.push(this.suites_[i]);
+ }
+ }
+ return topLevelSuites;
+};
+
+jasmine.Runner.prototype.results = function() {
+ return this.queue.results();
+};
+/**
+ * Internal representation of a Jasmine specification, or test.
+ *
+ * @constructor
+ * @param {jasmine.Env} env
+ * @param {jasmine.Suite} suite
+ * @param {String} description
+ */
+jasmine.Spec = function(env, suite, description) {
+ if (!env) {
+ throw new Error('jasmine.Env() required');
+ }
+ if (!suite) {
+ throw new Error('jasmine.Suite() required');
+ }
+ var spec = this;
+ spec.id = env.nextSpecId ? env.nextSpecId() : null;
+ spec.env = env;
+ spec.suite = suite;
+ spec.description = description;
+ spec.queue = new jasmine.Queue(env);
+
+ spec.afterCallbacks = [];
+ spec.spies_ = [];
+
+ spec.results_ = new jasmine.NestedResults();
+ spec.results_.description = description;
+ spec.matchersClass = null;
+};
+
+jasmine.Spec.prototype.getFullName = function() {
+ return this.suite.getFullName() + ' ' + this.description + '.';
+};
+
+
+jasmine.Spec.prototype.results = function() {
+ return this.results_;
+};
+
+/**
+ * All parameters are pretty-printed and concatenated together, then written to the spec's output.
+ *
+ * Be careful not to leave calls to <code>jasmine.log</code> in production code.
+ */
+jasmine.Spec.prototype.log = function() {
+ return this.results_.log(arguments);
+};
+
+jasmine.Spec.prototype.runs = function (func) {
+ var block = new jasmine.Block(this.env, func, this);
+ this.addToQueue(block);
+ return this;
+};
+
+jasmine.Spec.prototype.addToQueue = function (block) {
+ if (this.queue.isRunning()) {
+ this.queue.insertNext(block);
+ } else {
+ this.queue.add(block);
+ }
+};
+
+/**
+ * @param {jasmine.ExpectationResult} result
+ */
+jasmine.Spec.prototype.addMatcherResult = function(result) {
+ this.results_.addResult(result);
+};
+
+jasmine.Spec.prototype.expect = function(actual) {
+ var positive = new (this.getMatchersClass_())(this.env, actual, this);
+ positive.not = new (this.getMatchersClass_())(this.env, actual, this, true);
+ return positive;
+};
+
+/**
+ * Waits a fixed time period before moving to the next block.
+ *
+ * @deprecated Use waitsFor() instead
+ * @param {Number} timeout milliseconds to wait
+ */
+jasmine.Spec.prototype.waits = function(timeout) {
+ var waitsFunc = new jasmine.WaitsBlock(this.env, timeout, this);
+ this.addToQueue(waitsFunc);
+ return this;
+};
+
+/**
+ * Waits for the latchFunction to return true before proceeding to the next block.
+ *
+ * @param {Function} latchFunction
+ * @param {String} optional_timeoutMessage
+ * @param {Number} optional_timeout
+ */
+jasmine.Spec.prototype.waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) {
+ var latchFunction_ = null;
+ var optional_timeoutMessage_ = null;
+ var optional_timeout_ = null;
+
+ for (var i = 0; i < arguments.length; i++) {
+ var arg = arguments[i];
+ switch (typeof arg) {
+ case 'function':
+ latchFunction_ = arg;
+ break;
+ case 'string':
+ optional_timeoutMessage_ = arg;
+ break;
+ case 'number':
+ optional_timeout_ = arg;
+ break;
+ }
+ }
+
+ var waitsForFunc = new jasmine.WaitsForBlock(this.env, optional_timeout_, latchFunction_, optional_timeoutMessage_, this);
+ this.addToQueue(waitsForFunc);
+ return this;
+};
+
+jasmine.Spec.prototype.fail = function (e) {
+ var expectationResult = new jasmine.ExpectationResult({
+ passed: false,
+ message: e ? jasmine.util.formatException(e) : 'Exception',
+ trace: { stack: e.stack }
+ });
+ this.results_.addResult(expectationResult);
+};
+
+jasmine.Spec.prototype.getMatchersClass_ = function() {
+ return this.matchersClass || this.env.matchersClass;
+};
+
+jasmine.Spec.prototype.addMatchers = function(matchersPrototype) {
+ var parent = this.getMatchersClass_();
+ var newMatchersClass = function() {
+ parent.apply(this, arguments);
+ };
+ jasmine.util.inherit(newMatchersClass, parent);
+ jasmine.Matchers.wrapInto_(matchersPrototype, newMatchersClass);
+ this.matchersClass = newMatchersClass;
+};
+
+jasmine.Spec.prototype.finishCallback = function() {
+ this.env.reporter.reportSpecResults(this);
+};
+
+jasmine.Spec.prototype.finish = function(onComplete) {
+ this.removeAllSpies();
+ this.finishCallback();
+ if (onComplete) {
+ onComplete();
+ }
+};
+
+jasmine.Spec.prototype.after = function(doAfter) {
+ if (this.queue.isRunning()) {
+ this.queue.add(new jasmine.Block(this.env, doAfter, this));
+ } else {
+ this.afterCallbacks.unshift(doAfter);
+ }
+};
+
+jasmine.Spec.prototype.execute = function(onComplete) {
+ var spec = this;
+ if (!spec.env.specFilter(spec)) {
+ spec.results_.skipped = true;
+ spec.finish(onComplete);
+ return;
+ }
+
+ this.env.reporter.reportSpecStarting(this);
+
+ spec.env.currentSpec = spec;
+
+ spec.addBeforesAndAftersToQueue();
+
+ spec.queue.start(function () {
+ spec.finish(onComplete);
+ });
+};
+
+jasmine.Spec.prototype.addBeforesAndAftersToQueue = function() {
+ var runner = this.env.currentRunner();
+ var i;
+
+ for (var suite = this.suite; suite; suite = suite.parentSuite) {
+ for (i = 0; i < suite.before_.length; i++) {
+ this.queue.addBefore(new jasmine.Block(this.env, suite.before_[i], this));
+ }
+ }
+ for (i = 0; i < runner.before_.length; i++) {
+ this.queue.addBefore(new jasmine.Block(this.env, runner.before_[i], this));
+ }
+ for (i = 0; i < this.afterCallbacks.length; i++) {
+ this.queue.add(new jasmine.Block(this.env, this.afterCallbacks[i], this));
+ }
+ for (suite = this.suite; suite; suite = suite.parentSuite) {
+ for (i = 0; i < suite.after_.length; i++) {
+ this.queue.add(new jasmine.Block(this.env, suite.after_[i], this));
+ }
+ }
+ for (i = 0; i < runner.after_.length; i++) {
+ this.queue.add(new jasmine.Block(this.env, runner.after_[i], this));
+ }
+};
+
+jasmine.Spec.prototype.explodes = function() {
+ throw 'explodes function should not have been called';
+};
+
+jasmine.Spec.prototype.spyOn = function(obj, methodName, ignoreMethodDoesntExist) {
+ if (obj == jasmine.undefined) {
+ throw "spyOn could not find an object to spy upon for " + methodName + "()";
+ }
+
+ if (!ignoreMethodDoesntExist && obj[methodName] === jasmine.undefined) {
+ throw methodName + '() method does not exist';
+ }
+
+ if (!ignoreMethodDoesntExist && obj[methodName] && obj[methodName].isSpy) {
+ throw new Error(methodName + ' has already been spied upon');
+ }
+
+ var spyObj = jasmine.createSpy(methodName);
+
+ this.spies_.push(spyObj);
+ spyObj.baseObj = obj;
+ spyObj.methodName = methodName;
+ spyObj.originalValue = obj[methodName];
+
+ obj[methodName] = spyObj;
+
+ return spyObj;
+};
+
+jasmine.Spec.prototype.removeAllSpies = function() {
+ for (var i = 0; i < this.spies_.length; i++) {
+ var spy = this.spies_[i];
+ spy.baseObj[spy.methodName] = spy.originalValue;
+ }
+ this.spies_ = [];
+};
+
+/**
+ * Internal representation of a Jasmine suite.
+ *
+ * @constructor
+ * @param {jasmine.Env} env
+ * @param {String} description
+ * @param {Function} specDefinitions
+ * @param {jasmine.Suite} parentSuite
+ */
+jasmine.Suite = function(env, description, specDefinitions, parentSuite) {
+ var self = this;
+ self.id = env.nextSuiteId ? env.nextSuiteId() : null;
+ self.description = description;
+ self.queue = new jasmine.Queue(env);
+ self.parentSuite = parentSuite;
+ self.env = env;
+ self.before_ = [];
+ self.after_ = [];
+ self.children_ = [];
+ self.suites_ = [];
+ self.specs_ = [];
+};
+
+jasmine.Suite.prototype.getFullName = function() {
+ var fullName = this.description;
+ for (var parentSuite = this.parentSuite; parentSuite; parentSuite = parentSuite.parentSuite) {
+ fullName = parentSuite.description + ' ' + fullName;
+ }
+ return fullName;
+};
+
+jasmine.Suite.prototype.finish = function(onComplete) {
+ this.env.reporter.reportSuiteResults(this);
+ this.finished = true;
+ if (typeof(onComplete) == 'function') {
+ onComplete();
+ }
+};
+
+jasmine.Suite.prototype.beforeEach = function(beforeEachFunction) {
+ beforeEachFunction.typeName = 'beforeEach';
+ this.before_.unshift(beforeEachFunction);
+};
+
+jasmine.Suite.prototype.afterEach = function(afterEachFunction) {
+ afterEachFunction.typeName = 'afterEach';
+ this.after_.unshift(afterEachFunction);
+};
+
+jasmine.Suite.prototype.results = function() {
+ return this.queue.results();
+};
+
+jasmine.Suite.prototype.add = function(suiteOrSpec) {
+ this.children_.push(suiteOrSpec);
+ if (suiteOrSpec instanceof jasmine.Suite) {
+ this.suites_.push(suiteOrSpec);
+ this.env.currentRunner().addSuite(suiteOrSpec);
+ } else {
+ this.specs_.push(suiteOrSpec);
+ }
+ this.queue.add(suiteOrSpec);
+};
+
+jasmine.Suite.prototype.specs = function() {
+ return this.specs_;
+};
+
+jasmine.Suite.prototype.suites = function() {
+ return this.suites_;
+};
+
+jasmine.Suite.prototype.children = function() {
+ return this.children_;
+};
+
+jasmine.Suite.prototype.execute = function(onComplete) {
+ var self = this;
+ this.queue.start(function () {
+ self.finish(onComplete);
+ });
+};
+jasmine.WaitsBlock = function(env, timeout, spec) {
+ this.timeout = timeout;
+ jasmine.Block.call(this, env, null, spec);
+};
+
+jasmine.util.inherit(jasmine.WaitsBlock, jasmine.Block);
+
+jasmine.WaitsBlock.prototype.execute = function (onComplete) {
+ if (jasmine.VERBOSE) {
+ this.env.reporter.log('>> Jasmine waiting for ' + this.timeout + ' ms...');
+ }
+ this.env.setTimeout(function () {
+ onComplete();
+ }, this.timeout);
+};
+/**
+ * A block which waits for some condition to become true, with timeout.
+ *
+ * @constructor
+ * @extends jasmine.Block
+ * @param {jasmine.Env} env The Jasmine environment.
+ * @param {Number} timeout The maximum time in milliseconds to wait for the condition to become true.
+ * @param {Function} latchFunction A function which returns true when the desired condition has been met.
+ * @param {String} message The message to display if the desired condition hasn't been met within the given time period.
+ * @param {jasmine.Spec} spec The Jasmine spec.
+ */
+jasmine.WaitsForBlock = function(env, timeout, latchFunction, message, spec) {
+ this.timeout = timeout || env.defaultTimeoutInterval;
+ this.latchFunction = latchFunction;
+ this.message = message;
+ this.totalTimeSpentWaitingForLatch = 0;
+ jasmine.Block.call(this, env, null, spec);
+};
+jasmine.util.inherit(jasmine.WaitsForBlock, jasmine.Block);
+
+jasmine.WaitsForBlock.TIMEOUT_INCREMENT = 10;
+
+jasmine.WaitsForBlock.prototype.execute = function(onComplete) {
+ if (jasmine.VERBOSE) {
+ this.env.reporter.log('>> Jasmine waiting for ' + (this.message || 'something to happen'));
+ }
+ var latchFunctionResult;
+ try {
+ latchFunctionResult = this.latchFunction.apply(this.spec);
+ } catch (e) {
+ this.spec.fail(e);
+ onComplete();
+ return;
+ }
+
+ if (latchFunctionResult) {
+ onComplete();
+ } else if (this.totalTimeSpentWaitingForLatch >= this.timeout) {
+ var message = 'timed out after ' + this.timeout + ' msec waiting for ' + (this.message || 'something to happen');
+ this.spec.fail({
+ name: 'timeout',
+ message: message
+ });
+
+ this.abort = true;
+ onComplete();
+ } else {
+ this.totalTimeSpentWaitingForLatch += jasmine.WaitsForBlock.TIMEOUT_INCREMENT;
+ var self = this;
+ this.env.setTimeout(function() {
+ self.execute(onComplete);
+ }, jasmine.WaitsForBlock.TIMEOUT_INCREMENT);
+ }
+};
+// Mock setTimeout, clearTimeout
+// Contributed by Pivotal Computer Systems, www.pivotalsf.com
+
+jasmine.FakeTimer = function() {
+ this.reset();
+
+ var self = this;
+ self.setTimeout = function(funcToCall, millis) {
+ self.timeoutsMade++;
+ self.scheduleFunction(self.timeoutsMade, funcToCall, millis, false);
+ return self.timeoutsMade;
+ };
+
+ self.setInterval = function(funcToCall, millis) {
+ self.timeoutsMade++;
+ self.scheduleFunction(self.timeoutsMade, funcToCall, millis, true);
+ return self.timeoutsMade;
+ };
+
+ self.clearTimeout = function(timeoutKey) {
+ self.scheduledFunctions[timeoutKey] = jasmine.undefined;
+ };
+
+ self.clearInterval = function(timeoutKey) {
+ self.scheduledFunctions[timeoutKey] = jasmine.undefined;
+ };
+
+};
+
+jasmine.FakeTimer.prototype.reset = function() {
+ this.timeoutsMade = 0;
+ this.scheduledFunctions = {};
+ this.nowMillis = 0;
+};
+
+jasmine.FakeTimer.prototype.tick = function(millis) {
+ var oldMillis = this.nowMillis;
+ var newMillis = oldMillis + millis;
+ this.runFunctionsWithinRange(oldMillis, newMillis);
+ this.nowMillis = newMillis;
+};
+
+jasmine.FakeTimer.prototype.runFunctionsWithinRange = function(oldMillis, nowMillis) {
+ var scheduledFunc;
+ var funcsToRun = [];
+ for (var timeoutKey in this.scheduledFunctions) {
+ scheduledFunc = this.scheduledFunctions[timeoutKey];
+ if (scheduledFunc != jasmine.undefined &&
+ scheduledFunc.runAtMillis >= oldMillis &&
+ scheduledFunc.runAtMillis <= nowMillis) {
+ funcsToRun.push(scheduledFunc);
+ this.scheduledFunctions[timeoutKey] = jasmine.undefined;
+ }
+ }
+
+ if (funcsToRun.length > 0) {
+ funcsToRun.sort(function(a, b) {
+ return a.runAtMillis - b.runAtMillis;
+ });
+ for (var i = 0; i < funcsToRun.length; ++i) {
+ try {
+ var funcToRun = funcsToRun[i];
+ this.nowMillis = funcToRun.runAtMillis;
+ funcToRun.funcToCall();
+ if (funcToRun.recurring) {
+ this.scheduleFunction(funcToRun.timeoutKey,
+ funcToRun.funcToCall,
+ funcToRun.millis,
+ true);
+ }
+ } catch(e) {
+ }
+ }
+ this.runFunctionsWithinRange(oldMillis, nowMillis);
+ }
+};
+
+jasmine.FakeTimer.prototype.scheduleFunction = function(timeoutKey, funcToCall, millis, recurring) {
+ this.scheduledFunctions[timeoutKey] = {
+ runAtMillis: this.nowMillis + millis,
+ funcToCall: funcToCall,
+ recurring: recurring,
+ timeoutKey: timeoutKey,
+ millis: millis
+ };
+};
+
+/**
+ * @namespace
+ */
+jasmine.Clock = {
+ defaultFakeTimer: new jasmine.FakeTimer(),
+
+ reset: function() {
+ jasmine.Clock.assertInstalled();
+ jasmine.Clock.defaultFakeTimer.reset();
+ },
+
+ tick: function(millis) {
+ jasmine.Clock.assertInstalled();
+ jasmine.Clock.defaultFakeTimer.tick(millis);
+ },
+
+ runFunctionsWithinRange: function(oldMillis, nowMillis) {
+ jasmine.Clock.defaultFakeTimer.runFunctionsWithinRange(oldMillis, nowMillis);
+ },
+
+ scheduleFunction: function(timeoutKey, funcToCall, millis, recurring) {
+ jasmine.Clock.defaultFakeTimer.scheduleFunction(timeoutKey, funcToCall, millis, recurring);
+ },
+
+ useMock: function() {
+ if (!jasmine.Clock.isInstalled()) {
+ var spec = jasmine.getEnv().currentSpec;
+ spec.after(jasmine.Clock.uninstallMock);
+
+ jasmine.Clock.installMock();
+ }
+ },
+
+ installMock: function() {
+ jasmine.Clock.installed = jasmine.Clock.defaultFakeTimer;
+ },
+
+ uninstallMock: function() {
+ jasmine.Clock.assertInstalled();
+ jasmine.Clock.installed = jasmine.Clock.real;
+ },
+
+ real: {
+ setTimeout: jasmine.getGlobal().setTimeout,
+ clearTimeout: jasmine.getGlobal().clearTimeout,
+ setInterval: jasmine.getGlobal().setInterval,
+ clearInterval: jasmine.getGlobal().clearInterval
+ },
+
+ assertInstalled: function() {
+ if (!jasmine.Clock.isInstalled()) {
+ throw new Error("Mock clock is not installed, use jasmine.Clock.useMock()");
+ }
+ },
+
+ isInstalled: function() {
+ return jasmine.Clock.installed == jasmine.Clock.defaultFakeTimer;
+ },
+
+ installed: null
+};
+jasmine.Clock.installed = jasmine.Clock.real;
+
+//else for IE support
+jasmine.getGlobal().setTimeout = function(funcToCall, millis) {
+ if (jasmine.Clock.installed.setTimeout.apply) {
+ return jasmine.Clock.installed.setTimeout.apply(this, arguments);
+ } else {
+ return jasmine.Clock.installed.setTimeout(funcToCall, millis);
+ }
+};
+
+jasmine.getGlobal().setInterval = function(funcToCall, millis) {
+ if (jasmine.Clock.installed.setInterval.apply) {
+ return jasmine.Clock.installed.setInterval.apply(this, arguments);
+ } else {
+ return jasmine.Clock.installed.setInterval(funcToCall, millis);
+ }
+};
+
+jasmine.getGlobal().clearTimeout = function(timeoutKey) {
+ if (jasmine.Clock.installed.clearTimeout.apply) {
+ return jasmine.Clock.installed.clearTimeout.apply(this, arguments);
+ } else {
+ return jasmine.Clock.installed.clearTimeout(timeoutKey);
+ }
+};
+
+jasmine.getGlobal().clearInterval = function(timeoutKey) {
+ if (jasmine.Clock.installed.clearTimeout.apply) {
+ return jasmine.Clock.installed.clearInterval.apply(this, arguments);
+ } else {
+ return jasmine.Clock.installed.clearInterval(timeoutKey);
+ }
+};
+
+jasmine.version_= {
+ "major": 1,
+ "minor": 1,
+ "build": 0,
+ "revision": 1308187385,
+ "rc": 1
+}
diff --git a/webapp/webapp/static/jasmine-latest/jasmine_favicon.png b/webapp/webapp/static/jasmine-latest/jasmine_favicon.png
new file mode 100644
index 0000000..218f3b4
--- /dev/null
+++ b/webapp/webapp/static/jasmine-latest/jasmine_favicon.png
Binary files differ
diff --git a/webapp/webapp/static/js/bootstrap-fileupload.js b/webapp/webapp/static/js/bootstrap-fileupload.js
new file mode 100755
index 0000000..97a6da5
--- /dev/null
+++ b/webapp/webapp/static/js/bootstrap-fileupload.js
@@ -0,0 +1,169 @@
+/* ===========================================================
+ * bootstrap-fileupload.js j2
+ * http://jasny.github.com/bootstrap/javascript.html#fileupload
+ * ===========================================================
+ * Copyright 2012 Jasny BV, Netherlands.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License")
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================================================== */
+
+!function ($) {
+
+ "use strict"; // jshint ;_
+
+ /* FILEUPLOAD PUBLIC CLASS DEFINITION
+ * ================================= */
+
+ var Fileupload = function (element, options) {
+ this.$element = $(element)
+ this.type = this.$element.data('uploadtype') || (this.$element.find('.thumbnail').length > 0 ? "image" : "file")
+
+ this.$input = this.$element.find(':file')
+ if (this.$input.length === 0) return
+
+ this.name = this.$input.attr('name') || options.name
+
+ this.$hidden = this.$element.find('input[type=hidden][name="'+this.name+'"]')
+ if (this.$hidden.length === 0) {
+ this.$hidden = $('<input type="hidden" />')
+ this.$element.prepend(this.$hidden)
+ }
+
+ this.$preview = this.$element.find('.fileupload-preview')
+ var height = this.$preview.css('height')
+ if (this.$preview.css('display') != 'inline' && height != '0px' && height != 'none') this.$preview.css('line-height', height)
+
+ this.original = {
+ 'exists': this.$element.hasClass('fileupload-exists'),
+ 'preview': this.$preview.html(),
+ 'hiddenVal': this.$hidden.val()
+ }
+
+ this.$remove = this.$element.find('[data-dismiss="fileupload"]')
+
+ this.$element.find('[data-trigger="fileupload"]').on('click.fileupload', $.proxy(this.trigger, this))
+
+ this.listen()
+ }
+
+ Fileupload.prototype = {
+
+ listen: function() {
+ this.$input.on('change.fileupload', $.proxy(this.change, this))
+ $(this.$input[0].form).on('reset.fileupload', $.proxy(this.reset, this))
+ if (this.$remove) this.$remove.on('click.fileupload', $.proxy(this.clear, this))
+ },
+
+ change: function(e, invoked) {
+ if (invoked === 'clear') return
+
+ var file = e.target.files !== undefined ? e.target.files[0] : (e.target.value ? { name: e.target.value.replace(/^.+\\/, '') } : null)
+
+ if (!file) {
+ this.clear()
+ return
+ }
+
+ this.$hidden.val('')
+ this.$hidden.attr('name', '')
+ this.$input.attr('name', this.name)
+
+ if (this.type === "image" && this.$preview.length > 0 && (typeof file.type !== "undefined" ? file.type.match('image.*') : file.name.match(/\.(gif|png|jpe?g)$/i)) && typeof FileReader !== "undefined") {
+ var reader = new FileReader()
+ var preview = this.$preview
+ var element = this.$element
+
+ reader.onload = function(e) {
+ preview.html('<img src="' + e.target.result + '" ' + (preview.css('max-height') != 'none' ? 'style="max-height: ' + preview.css('max-height') + ';"' : '') + ' />')
+ element.addClass('fileupload-exists').removeClass('fileupload-new')
+ }
+
+ reader.readAsDataURL(file)
+ } else {
+ this.$preview.text(file.name)
+ this.$element.addClass('fileupload-exists').removeClass('fileupload-new')
+ }
+ },
+
+ clear: function(e) {
+ this.$hidden.val('')
+ this.$hidden.attr('name', this.name)
+ this.$input.attr('name', '')
+
+ //ie8+ doesn't support changing the value of input with type=file so clone instead
+ if (navigator.userAgent.match(/msie/i)){
+ var inputClone = this.$input.clone(true);
+ this.$input.after(inputClone);
+ this.$input.remove();
+ this.$input = inputClone;
+ }else{
+ this.$input.val('')
+ }
+
+ this.$preview.html('')
+ this.$element.addClass('fileupload-new').removeClass('fileupload-exists')
+
+ if (e) {
+ this.$input.trigger('change', [ 'clear' ])
+ e.preventDefault()
+ }
+ },
+
+ reset: function(e) {
+ this.clear()
+
+ this.$hidden.val(this.original.hiddenVal)
+ this.$preview.html(this.original.preview)
+
+ if (this.original.exists) this.$element.addClass('fileupload-exists').removeClass('fileupload-new')
+ else this.$element.addClass('fileupload-new').removeClass('fileupload-exists')
+ },
+
+ trigger: function(e) {
+ this.$input.trigger('click')
+ e.preventDefault()
+ }
+ }
+
+
+ /* FILEUPLOAD PLUGIN DEFINITION
+ * =========================== */
+
+ $.fn.fileupload = function (options) {
+ return this.each(function () {
+ var $this = $(this)
+ , data = $this.data('fileupload')
+ if (!data) $this.data('fileupload', (data = new Fileupload(this, options)))
+ if (typeof options == 'string') data[options]()
+ })
+ }
+
+ $.fn.fileupload.Constructor = Fileupload
+
+
+ /* FILEUPLOAD DATA-API
+ * ================== */
+
+ $(document).on('click.fileupload.data-api', '[data-provides="fileupload"]', function (e) {
+ var $this = $(this)
+ if ($this.data('fileupload')) return
+ $this.fileupload($this.data())
+
+ var $target = $(e.target).closest('[data-dismiss="fileupload"],[data-trigger="fileupload"]');
+ if ($target.length > 0) {
+ $target.trigger('click.fileupload')
+ e.preventDefault()
+ }
+ })
+
+}(window.jQuery);
diff --git a/webapp/webapp/static/js/dynamic_structure.js b/webapp/webapp/static/js/dynamic_structure.js
new file mode 100644
index 0000000..69f2223
--- /dev/null
+++ b/webapp/webapp/static/js/dynamic_structure.js
@@ -0,0 +1,479 @@
+TEMPLATES = {};
+
+/// METHODS FOR OPTIONS ///
+
+var factoryOptionDefault = function(group_order, field_widget, id, default_value) {
+ var field_order = parseInt(field_widget.find(".field_order").attr("value"));
+
+ if (id == undefined){
+ OFFSET_OPTION_ID++;
+ id = OFFSET_OPTION_ID
+ }
+
+ var container = field_widget.find('.WFieldOptions_container'),
+ row_fluid = $('<div class="row-fluid"></div>');
+
+ var option_default = $(
+ Mustache.render(TEMPLATES['field_option_default'], {
+ "id": id,
+ "value": default_value || '',
+ "group_order": group_order,
+ "field_order": field_order
+ })
+ );
+
+ row_fluid.append(option_default);
+ container.append(row_fluid);
+};
+
+
+var factoryImageOptionUpload = function(id, value, container, group_order, field_order) {
+
+ var option = $(
+ Mustache.render(TEMPLATES['field_option_image_upload'], {
+ "id": id,
+ "weight": value['weight'],
+ "group_order": group_order,
+ "field_order": field_order
+ })
+ ),
+ row_fluid = container.find('.row-fluid');
+
+ if (row_fluid.size() == 0)
+ row_fluid = $('<div class="row-fluid"></div>');
+
+ // Draggable option ID to droppable dependence field
+ option.find(".draggable").draggable({'helper': 'clone'});
+
+ row_fluid.append(option);
+ container.append(row_fluid);
+};
+
+var get_opt_thumb = function(poll_id, img_name){
+ var thumb = "";
+
+ $.ajax({
+ url: "/polls/option/thumb/" + poll_id + "/" + img_name + "/",
+ success: function(result) {
+ thumb = result;
+ },
+ async: false
+ });
+
+ return thumb;
+}
+
+var factoryImageOptionThumbnail = function(id, value, container, group_order, field_order) {
+
+ var option = $(
+ Mustache.render(TEMPLATES['field_option_image_thumbnail'], {
+ "id": id,
+ "IMAGE_OPTIONS_TMP_MEDIA_URL": IMAGE_OPTIONS_TMP_MEDIA_URL,
+ "img_name": value['img_name'],
+ "weight": value['weight'],
+ "group_order": group_order,
+ "field_order": field_order
+ })
+ ),
+ row_fluid = container.find('.row-fluid');
+
+ if (row_fluid.size() == 0)
+ row_fluid = $('<div class="row-fluid"></div>');
+
+ // Get remove option button and bind with remove option event
+ option.find('.WFieldImageOptions_remove_button').on('click', function(event) {
+ event.preventDefault();
+
+ if(confirm('La opción ' + id + ' va a ser eliminada.')) {
+ var container_row_fuild = option.parent(".row-fluid");
+
+ option.remove();
+ }
+ });
+
+ // Append new img option
+ var thumb = get_opt_thumb(POLL_ID, value['img_name']),
+ img_src = $(thumb).find('img').attr('img_src'),
+ img = '<img src="' + img_src + '" />';
+
+ $(option.find(".img_container")).append(thumb);
+ $(option.find('img')).popover({
+ title: "<b>ID</b>: " + id + ", <b>Peso</b>: " + value['weight'],
+ content: img,
+ html: true,
+ trigger: "hover"
+ });
+
+ // Draggable option ID to droppable dependence field
+ option.find(".draggable").draggable({'helper': 'clone'});
+
+ row_fluid.append(option);
+ container.append(row_fluid);
+};
+
+
+var factoryOption = function(id, value, container, group_order, field_order) {
+
+ var option = $(
+ Mustache.render(TEMPLATES['field_option'], {
+ "id": id,
+ "value": value['text'],
+ "weight": value['weight'],
+ "group_order": group_order,
+ "field_order": field_order
+ })
+ );
+
+ // Get remove option button and bind with remove option event
+ option.find('.WFieldOptions_remove').on('click', function(event) {
+ event.preventDefault();
+
+ if(confirm('La opción ' + id + ' va a ser eliminada.')) {
+ var container_row_fuild = option.parent(".row-fluid");
+
+ option.remove();
+
+ /* Check for remove row of options */
+ if (container_row_fuild.contents().length == 0) {
+ container_row_fuild.remove();
+ }
+ }
+ });
+
+ // Draggable option ID to droppable dependence field
+ option.find(".draggable").draggable({'helper': 'clone'});
+
+ // Append new option
+ row_fluid = $('<div class="row-fluid"></div>');
+ row_fluid.append(option);
+ container.append(row_fluid);
+};
+
+/// METHODS FOR FIELDS ///
+
+var _updateFieldInputs = function(inputs, pattern){
+ var field_order,
+ new_order,
+ new_name;
+
+ $.each(inputs, function(i, input){
+ name = $(this).attr('name');
+ field_order = parseInt(name.match(pattern)[2]);
+ new_order = field_order - 1;
+ new_name = name.replace(pattern, '$1' + new_order );
+ $(this).attr('name', new_name);
+
+ // Update order input hidden
+ // Corregir esto de alguna otra manera
+ if ($(input).attr('type') == 'hidden'){
+ $(input).attr('value', new_order);
+ }
+ });
+};
+
+var bindFieldRemoveButton = function(remove_button) {
+ remove_button.on('click', function(event){
+ event.preventDefault();
+
+ if(confirm('¿Esta seguro que quiere eliminar esta pregunta?')) {
+
+ var field_widget = $('.field').has(remove_button),
+ following_siblings = field_widget.nextAll('.field'),
+ pattern = /(.fields.)(\d+)/;
+
+ // Update order of groups, very important!
+ var all_inputs = following_siblings.find(":input:not(:button)");
+ _updateFieldInputs(all_inputs, pattern);
+
+ field_widget.remove();
+
+ // Unbind add_option buttons and bind again
+ var add_option_buttons = field_widget.find('.WFieldOptions_add_button');
+ $.each(add_option_buttons, function(i, add_option_button){
+ $(add_option_button).unbind('click');
+ bindFieldAddOptionButton(add_option_button);
+ });
+ }
+
+ });
+};
+
+var bindFieldAddOptionButton = function(add_option_button) {
+ $(add_option_button).find("button").on('click', function(event) {
+ event.preventDefault();
+
+ var field_widget = $('.field').has(add_option_button),
+ group_widget = $('.group').has(field_widget),
+ container_widget = field_widget.find('.WGroup_field_containter'),
+ group_order = parseInt(group_widget.find(".group_order").attr("value")),
+ field_order = parseInt(field_widget.find(".field_order").attr("value"));
+
+ // Add a new and empty option widget
+ options_container = field_widget.find('.WFieldOptions_container');
+ OFFSET_OPTION_ID++;
+ empty_option_widget = {'name': ''};
+
+ // Get current selected widget type for current field
+ var current_widget_type = field_widget.find(".WFieldWidgetType").attr('value');
+
+ if ($.inArray(current_widget_type, WITH_IMAGES) == -1){
+ factoryOption(OFFSET_OPTION_ID, empty_option_widget, options_container, group_order, field_order );
+ } else {
+ factoryImageOptionUpload(OFFSET_OPTION_ID, empty_option_widget, options_container, group_order, field_order );
+ }
+ });
+}
+
+var bindFieldWidgetTypeSelectBox = function(widget_types_select_box) {
+ $(widget_types_select_box).on('change', function(event){
+ event.preventDefault();
+
+ var field_widget = $('.field').has(widget_types_select_box),
+ group_widget = $('.group').has(field_widget),
+ group_order = parseInt(group_widget.find(".group_order").attr("value")),
+ container = field_widget.find('.WFieldOptions_container'),
+ buttons_container = field_widget.find('.WFieldAddOptionButton_container'),
+ field_order = parseInt(field_widget.find(".field_order").attr("value")),
+ add_option_button;
+
+ // Get current selected widget type for current field
+ var current_widget_type = $(this).attr('value');
+
+ // Clear all options
+ container.contents().remove();
+
+ if ($.inArray(current_widget_type, WITH_OPTIONS) == -1) {
+ // Not need options -> remove add_option_button and show option default
+ add_option_button = buttons_container.find('.WFieldOptions_add_button').find('button');
+ add_option_button.remove();
+
+ factoryOptionDefault(group_order, field_widget, undefined, undefined);
+ } else {
+ if ((buttons_container).find('button').length == 0) {
+ add_option_button = $(Mustache.render(TEMPLATES['field_add_option_button'], {}));
+ // Append button to add new option if not exist already
+ buttons_container.append(add_option_button);
+ // Bind add option button
+ bindFieldAddOptionButton(add_option_button);
+ }
+ }
+
+ });
+};
+
+var factoryField = function(order, value) {
+ // Get field.widget_type
+ var widget_type = value['widget_type'],
+ group_order = value['group_order'],
+ errors = [];
+
+ // If errors, prepare it
+ if (value.hasOwnProperty('errors')){
+ $.each(value['errors'], function(i, error){
+ errors.push({'error': error});
+ });
+ }
+
+ // Preparing values for render widget type select box
+ widget_types = [].concat(WIDGET_TYPES);
+ var widget_types = $.each(widget_types, function(i, v){
+ if (widget_type == v['key']) {
+ $.extend(v, {"selected": true});
+ } else {
+ // TODO: ver como pasar por copia valores en javascript
+ delete v.selected;
+ }
+ }),
+ field_widget = $(
+ Mustache.render(TEMPLATES['field'], {
+ "order": order,
+ "name": value['name'],
+ "dependence": value['dependence'],
+ "errors": errors,
+ "visible_errors": errors.length,
+ "group_order": group_order,
+ "WIDGET_TYPES": widget_types,
+ })
+ );
+
+ // Get remove field button and bind remove field widget envent
+ var remove_button = field_widget.find('.WField_remove');
+ bindFieldRemoveButton(remove_button);
+
+ var add_option_button = field_widget.find('.WFieldOptions_add_button');
+ bindFieldAddOptionButton(add_option_button);
+
+ // Bind change widget type event
+ var widget_types_select_box = field_widget.find('.WFieldWidgetType');
+ bindFieldWidgetTypeSelectBox(widget_types_select_box);
+
+ // Adding add_option button if is needed
+ buttons_container = field_widget.find('.WFieldAddOptionButton_container');
+ if (widget_type && $.inArray(widget_type, WITH_OPTIONS) != -1) {
+ var add_option_button = $(Mustache.render(TEMPLATES['field_add_option_button'], {}));
+ buttons_container.append(add_option_button);
+ bindFieldAddOptionButton(add_option_button);
+ }
+
+ // Render options
+ if ($.inArray(widget_type, WITH_OPTIONS) != -1){
+ if ($.inArray(widget_type, WITH_IMAGES) == -1){
+ // Basic option
+ $.each(value['options'] || [], function(id, opt_value){
+ options_container = field_widget.find('.WFieldOptions_container');
+ factoryOption(id, opt_value, options_container, group_order, order);
+ });
+ } else {
+ // Image option
+ $.each(value['options'] || [], function(id, opt_value){
+ options_container = field_widget.find('.WFieldOptions_container');
+ if (opt_value.hasOwnProperty('img_name')){
+ factoryImageOptionThumbnail(id, opt_value, options_container, group_order, order);
+ } else {
+ factoryImageOptionUpload(id, opt_value, options_container, group_order, order);
+ }
+ });
+ }
+ } else {
+ if (value['options'] && Object.keys(value['options']).length){
+ $.each(value['options'] || [], function(id, opt_value){
+ factoryOptionDefault(group_order, field_widget, id, opt_value['text']);
+ });
+ } else {
+ factoryOptionDefault(group_order, field_widget, undefined, undefined);
+ }
+ }
+
+ var dependence = field_widget.find('[name*=".dependence"]');
+ if (dependence.attr("value") != "")
+ dependence.addClass("ui-state-highlight");
+
+ var droppable = field_widget.find(".droppable");
+ droppable.droppable({
+ drop: function( event, ui ) {
+ var value = ui.draggable[0]['innerText'];
+ $(this)
+ .addClass("ui-state-highlight")
+ .attr("value", value);
+ }
+ });
+ droppable.focusout(function(){
+ if ($(this).attr('value') == ""){
+ $(this).removeClass("ui-state-highlight");
+ }
+ });
+
+ // Show the field widget
+ var container = value['container'];
+ container.append(field_widget);
+};
+
+/// METHODS FOR GROUPS ///
+
+var _updateGroupInputs = function(inputs, pattern){
+ var group_order,
+ new_order,
+ new_name;
+
+ $.each(inputs, function(i, input){
+ name = $(this).attr('name');
+ group_order = parseInt(name.match(pattern)[2]);
+ new_order = group_order - 1;
+ new_name = name.replace(pattern, '$1' + new_order );
+ $(this).attr('name', new_name);
+
+ // Update order input hidden
+ order_input_name = 'groups.' + new_order + '.order';
+ if ($(input).attr('name') == order_input_name){
+ $(input).attr('value', new_order);
+ }
+ });
+};
+
+var bindGroupRemoveButton = function(remove_button) {
+ remove_button.on('click', function(event){
+ event.preventDefault();
+
+ if(confirm('¿Esta seguro que quiere eliminar este grupo?')){
+ var group_widget = $('.group').has(remove_button),
+ following_siblings = group_widget.nextAll('.group'),
+ pattern = /(groups.)(\d+)/;
+
+ // Update order of groups, very important!
+ var all_inputs = following_siblings.find(":input:not(:button)");
+ _updateGroupInputs(all_inputs, pattern);
+
+ // Remove group
+ group_widget.remove();
+
+ // Unbind add_field buttons and bind again
+ var add_field_buttons = $('.WGroup_add_field');
+ $.each(add_field_buttons, function(i, add_field_button){
+ $(add_field_button).unbind('click');
+ bindGroupAddFieldButton(add_field_button);
+ });
+ }
+
+ });
+};
+
+var bindGroupAddFieldButton = function(add_field_button) {
+ $(add_field_button).on('click', function(event) {
+ event.preventDefault();
+
+ var group_widget = $('.group').has(add_field_button),
+ container_widget = group_widget.find('.WGroup_field_containter'),
+ field_order = group_widget.find('.field').length,
+ group_order = parseInt(group_widget.find(":input[type='hidden']").attr("value"));
+
+ // Add a new and empty field widget
+ empty_field_widget = {
+ 'widget_type': '',
+ 'options': [],
+ 'container': container_widget,
+ 'group_order': group_order
+ }
+ field_widget = factoryField(field_order, empty_field_widget);
+ });
+}
+
+var factoryGroup = function(order, value) {
+
+ // Has errors? -> prepare errors dict
+ var errors = [];
+ if (value.hasOwnProperty('errors')){
+ $.each(value['errors'], function(i, error){
+ errors.push({'error': error});
+ });
+ }
+
+ // Render group widget
+ var group = $(
+ Mustache.render(TEMPLATES['group'], {
+ "order": order,
+ "name": value['name'],
+ "errors": errors,
+ "visible_errors": errors.length
+ })
+ );
+
+ // Get remove group button and bind remove group widget envent
+ var remove_button = group.find('.WGroup_remove');
+ bindGroupRemoveButton(remove_button);
+
+ // Render fields for current group
+ if (value.hasOwnProperty('fields')){
+ var field_container = group.find('.WGroup_field_containter');
+ var field_data = {'group_order': order, 'container': field_container};
+ $.each(value['fields'], function(x, y){ $.extend(y, field_data); });
+ $.each(value['fields'], factoryField);
+ }
+
+ // Get add field button and bind add field widget envent
+ var add_field_button = group.find('.WGroup_add_field');
+ bindGroupAddFieldButton(add_field_button);
+
+ // Show the group widget
+ group.prepend("<hr />");
+ container.append(group);
+}; \ No newline at end of file
diff --git a/webapp/webapp/templates/admin/base_site.html b/webapp/webapp/templates/admin/base_site.html
new file mode 100644
index 0000000..65696c0
--- /dev/null
+++ b/webapp/webapp/templates/admin/base_site.html
@@ -0,0 +1,19 @@
+{% extends "admin/base.html" %}
+{% load i18n %}
+
+
+{% block title %}{{ title }} | {% trans 'Project Smile account management' %}{% endblock %}
+
+{% block extrastyle %}
+<link href="{{ STATIC_URL }}css/custom_admin.css" rel="stylesheet">
+{% endblock %}
+
+{% block branding %}
+ <h1 id="site-name">{% trans 'Administración de cuentas de usuarios' %}</h1>
+{% endblock %}
+
+{% block nav-global %}
+<ul id="nav-global">
+ <span><a href="{% url polls:list %}"><i class="icon-chevron-left icon-white"></i>&nbsp;Volver a encuestas.</a></span>
+</ul>
+{% endblock %}
diff --git a/webapp/webapp/templates/base-main-public.html b/webapp/webapp/templates/base-main-public.html
new file mode 100644
index 0000000..2bf6dde
--- /dev/null
+++ b/webapp/webapp/templates/base-main-public.html
@@ -0,0 +1,18 @@
+{% extends "base-main.html" %}
+
+{% block extra_css %}
+ <style type="text/css">
+ body { padding-top: 45px; }
+ </style>
+{% endblock %}
+
+{% block navbar %}
+ <div class="navbar navbar-inverse navbar-fixed-top">
+ <div class="navbar-inner">
+ <div class="container">
+ <button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse"></button>
+ <a class="brand" href="#">Sitema de encuestas</a>
+ </div>
+ </div>
+ </div>
+{% endblock %} \ No newline at end of file
diff --git a/webapp/webapp/templates/base-main.html b/webapp/webapp/templates/base-main.html
index 5f52b82..0786415 100644
--- a/webapp/webapp/templates/base-main.html
+++ b/webapp/webapp/templates/base-main.html
@@ -1,8 +1,10 @@
+{% load i18n %}
+
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
- <title>{% block title %}{% endblock %}</title>
+ <title>{% block title %}Sistema de encuestas{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
@@ -13,6 +15,11 @@
<link href="{{ STATIC_URL }}css/jquery-ui.css" rel="stylesheet" />
<link href="{{ STATIC_URL }}css/polls.css" rel="stylesheet" />
+ <style type="text/css">
+ body {
+ padding-top: 50px;
+ }
+ </style>
{% block extra_css %}{% endblock %}
<!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
@@ -29,15 +36,53 @@
Mustache.tags = ['[[', ']]'];
</script>
</head>
-<body>
-</head>
+
<body>
<div class="container-fluid">
- <div class="row-fuild">
- <a id="lista_encuestas" class="btn btn-warning pull-right" href="{{ MEDIA_URL }}output" target="_blank" data-original-title="Listar estructuras de encuestas"><i class="icon-white icon-list-alt"></i></a>
+
+ {% block navbar %}
+ <div class="navbar navbar-inverse navbar-fixed-top">
+ <div class="navbar-inner">
+ <div class="container">
+ <button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse"></button>
+ <a class="brand" href="#">Sitema de encuestas</a>
+ <div class="nav-collapse collapse">
+ <ul class="nav">
+
+ {% url polls:list as polls_list_url %}
+ <li class="{% if request.path == polls_list_url %}active{% endif %}">
+ <a href="{{ polls_list_url }}"><i class="icon-white icon-list-alt"></i>&nbsp;Listado de encuestas</a>
+ </li>
+
+ {% url polls:add as polls_add_url %}
+ <li class="{% if request.path == polls_add_url %}active{% endif %}">
+ <a href="{{ polls_add_url }}"><i class="icon-white icon-plus"></i>&nbsp;Nueva encuesta</a>
+ </li>
+ </ul>
+ {% if user.is_authenticated %}
+ <ul class="nav pull-right">
+ <li class="divider-vertical"></li>
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown"><i class="icon-user icon-white"></i>&nbsp;<span>{{ user.username }}&nbsp;</span><b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ {% if user.is_superuser %}
+ <li><a class="float_r" href="{% url admin:index %}"><i class="icon-list"></i>&nbsp;{% trans "Cuentas de usuarios" %}</a></li>
+ {% endif %}
+ <li class="divider"></li>
+ <li><a class="float_r" href="{% url accounts:logout %}"><i class="icon-off"></i>&nbsp;{% trans "Logout" %}</a></li>
+ </ul>
+ </li>
+ </ul>
+ {% endif %}
+ </div>
+ </div>
+ </div>
</div>
+ {% endblock %}
+
<div class="row-fuild">
+ {% block messages %}{% endblock %}
{% block main_container %}{% endblock %}
</div>
</div>
diff --git a/webapp/webapp/urls.py b/webapp/webapp/urls.py
index d46ccc2..b2e9732 100644
--- a/webapp/webapp/urls.py
+++ b/webapp/webapp/urls.py
@@ -1,11 +1,18 @@
+import os
+
from django.conf.urls.defaults import patterns, include, url
from django.views.generic.base import RedirectView
from django.conf import settings
+from utils.decorators import *
+
urlpatterns = patterns('',
- url(r'^polls/', include('polls.urls', namespace="polls")),
- url(r'^$', RedirectView.as_view(url='polls/builder/'))
+ url(r'^admin/', include('custom_admin.urls')),
+ url(r'^accounts/', include('accounts.urls', namespace="accounts")),
+ url(r'^polls/', decorator_include(
+ user_account_required, 'polls.urls', namespace="polls")),
+ url(r'^$', RedirectView.as_view(url='polls/')),
)
if settings.DEBUG:
@@ -21,4 +28,12 @@ if settings.DEBUG:
'serve',
{"document_root": settings.MEDIA_ROOT, "show_indexes": True}
),
+
+ # Jasmine
+ url(r'^jasmine/mustache_templates/(?P<path>.*)$', 'serve', {
+ 'document_root': os.path.join(
+ settings.PROJECT_ROOT + '/../polls/templates/', "mustache",
+ )}
+ ),
+ url(r'jasmine/', include('django_jasmine.urls')),
)