commit ccd822a8eddc0631f6c98aa5740cb208f21d673e
Author: Georgios Gousios <gousiosg@gmail.com>
Date:   Fri May 13 16:41:04 2011 +0300

    Dynamic list of names-email for sending invitations

diff --git a/invitations/invitations.py b/invitations/invitations.py
index 2627339..3210fd5 100644
--- a/invitations/invitations.py
+++ b/invitations/invitations.py
@@ -1,35 +1,39 @@
 from django import forms
 from django.conf import settings
 from django.db import transaction
+from django.forms.util import ErrorList
 from django.http import HttpResponse, HttpResponseRedirect
 from django.shortcuts import render_to_response
+from django.template import Template
+from django.template.context import Context, RequestContext
 from django.template.loader import render_to_string
 from synnefo.api.common import method_not_allowed
 from synnefo.db.models import Invitations, SynnefoUser
 from synnefo.logic import users
 
 class InvitationForm(forms.Form):
-    emails = forms.Textarea
-
-    def send_emails(self, request):
-        if request.method == 'POST': # If the form has been submitted...
-            form = InvitationForm(request.POST) # A form bound to the POST data
-            if form.is_valid(): # All validation rules pass
-                # Process the data in form.cleaned_data
-                # ...
-                return HttpResponseRedirect('/thanks/') # Redirect after POST
-        else:
-            form = InvitationForm() # An unbound form
+    name = forms.CharField(required=True)
+    email = forms.EmailField(required=True)
+
+def send_emails(request):
+    if request.method == 'POST':
+        form = InvitationForm(request.POST)
+        if form.is_valid():
+            # Process the data in form.cleaned_data
+            return HttpResponseRedirect('/invitation/')
+    else:
+        form = InvitationForm() # An unbound form
 
-        return render_to_response('invitation.html', {'form': form,})
+    return render_to_response('invitations.html', {'form': form,})
 
 def inv_demux(request):
     if request.method == 'GET':
         invitations = Invitations.objects.filter(source = request.user)
-        data = render_to_string('invitations.html', {'invitations': invitations})
+        form = InvitationForm()
+        data = render_to_string('invitations.html', {'invitations': invitations, 'form':form})
         return  HttpResponse(data)
     elif request.method == 'POST':
-        f = InvitationForm(request)
+        return send_emails(request)
     else:
         method_not_allowed(request)
 
diff --git a/invitations/templates/invitations.html b/invitations/templates/invitations.html
index 1165721..4a0a82d 100644
--- a/invitations/templates/invitations.html
+++ b/invitations/templates/invitations.html
@@ -4,40 +4,43 @@
 <head>
     <title>Αποστολή προσκλήσεων σε χρήστες</title>
     <script src="/static/jquery.tools.min.js"></script>
-
+    <script src="/static/jquery.dynamicField.js"></script>
     <script type="text/javascript">
 
     $(document).ready(function() {
-        $("#emaillist").click(function(){
-            $("#emaillist").text('');
-        });
+         $("#invform #removable-name-container-1").dynamicField();
     });
 
     </script>
 
     <style type="text/css">
-        
+
     </style>
 </head>
 <body>
-{% if error_message %}<div id="error"><strong>{{ error_message }}</strong></div>{% endif %}
-
-<form id="" action="/invitations/" method="post">
-{% csrf_token %}
-        <label for="emaillist">Email για αποστολή προσκλήσεων (διαθέσιμες: 
-            <span id="remaining">{{ invitations.count }}</span>
-            )
-        </label>
-        <textarea id="emaillist" rows="24" cols="80">
-Εισάγετε τη λίστα των διευθύνσεων email στις οποίες θέλετε να αποσταλούν προσκλήσεις, μια σε κάθε γραμμή.
-Οι διευθύνσεις θα πρέπει να είναι στη μορφή: Όνομα Επίθετο <uname@domain.tld>, για παράδειγμα:
-
-Donald Knuth <knuth@stanford.edu>
-Dennis Ritchie <drm@bell.com>
-
-        </textarea>
-        <br/>
-        <input type="submit" value="Αποστολή Προσκλήσεων" />
+<form action="/invitation/" method="post" id="invform">
+    {% csrf_token %}
+    <div id="fieldheaders">
+        <span id="field_name_name">Όνομα</span>
+        <span id="field_email_name">Email</span>
+    </div>
+    <div id="fields">
+        <div id="removable-name-container-1" class="removable-field-row">
+            <label for="name_1">Name</label>
+            <input type="text" name="name_1" id="name_1" value="" class="textInput removable" />
+            <label for="email_1">Email</label>
+            <input type="text" name="email_1" id="email_1" value="" class="textInput removable" />
+            <img src="/static/spacer.gif" width="16" height="16" alt="" title="Remove This Item" class="" />
+        </div>
+
+        <div id="add-name-container" class="add-field-container">
+        <span class="add-field-trigger">
+            <img src="/static/add.gif" alt="" title="Add New Item" />
+            Add Row
+        </span>
+        </div>
+    </div>
+    <p><input type="submit" value="Αποστολή Προσκλήσεων" /></p>
 </form>
 
 <div id="invsent">
diff --git a/invitations/urls.py b/invitations/urls.py
index dfc9f31..87999b9 100644
--- a/invitations/urls.py
+++ b/invitations/urls.py
@@ -2,5 +2,6 @@ from django.conf.urls.defaults import patterns
 
 
 urlpatterns = patterns('synnefo.invitations.invitations',
-    (r'^$', 'inv_demux')
+    (r'^$', 'inv_demux'),
+    (r'^/$', 'inv_demux'),
 )
diff --git a/ui/static/add.gif b/ui/static/add.gif
new file mode 100644
index 0000000..30f7e52
Binary files /dev/null and b/ui/static/add.gif differ
diff --git a/ui/static/cross.png b/ui/static/cross.png
new file mode 100755
index 0000000..1514d51
Binary files /dev/null and b/ui/static/cross.png differ
diff --git a/ui/static/jquery.dynamicField.js b/ui/static/jquery.dynamicField.js
new file mode 100755
index 0000000..c238cf8
--- /dev/null
+++ b/ui/static/jquery.dynamicField.js
@@ -0,0 +1,134 @@
+/*
+*	jQuery dynamicField plugin
+*	Copyright 2009, Matt Quackenbush (http://www.quackfuzed.com/)
+*
+*	Find usage demos at http://www.quackfuzed.com/demos/jQuery/dynamicField/index.cfm)
+*
+*	Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
+*	and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
+*
+*	Version: 1.0
+*	Date:	 8/13/2009
+*/
+;(function($) {
+	$.fn.dynamicField = function(options) {
+		if ( $(this).attr("id") == undefined ) {
+			alert("The dynamicField plugin could not be initialized.\n\nPlease check the selector.");
+			return $;
+		}
+		
+		var f = $(this);
+		
+		var settings = $.extend({
+			maxFields: 5,
+			removeImgSrc: "/static/cross.png",
+			spacerImgSrc: "/static/spacer.gif",
+			addTriggerClass: "add-field-trigger",
+			removeImgClass: "remove-field-trigger",
+			hideClass: "hide",
+			cloneContainerId: f.attr("id").replace(/^(.+)([_-][0-9]+)$/,"$1"),
+			rowContainerClass: f.attr("class"),
+			labelText: f.children("label")
+							.html(),
+			baseName: f.children("input")
+								.attr("name")
+								.replace(/^(.+[_-])([0-9]+)$/,"$1"),
+            baseNames: baseNames(),
+			addContainerId: "add-" + f.children("input")
+								.attr("name")
+								.replace(/^(.+)([_-][0-9]+)$/,"$1")
+								.replace(/_/g,"-") + "-container"
+		},options);
+		
+		var getFields = function() {
+			return $("div." + settings.rowContainerClass);
+		};
+
+        function baseNames() {
+            var names = new Array();
+            $.each(f.children("input"), function(index, child){
+                var name = child.name.replace(/^(.+[_-])([0-9]+)$/,"$1")
+                names.push(name);
+            });
+            return names;
+        }
+		
+		// handle hide/show, etc
+		var addRemoveBtnCk = function() {
+			var fields = getFields();
+			var len = fields.length;
+			
+			fields.each(function(i,elem) {
+				$(elem)
+					.children("img")
+					.attr({
+						"src":(len == 1) ? settings.spacerImgSrc : settings.removeImgSrc,
+						"class":(len == 1) ? "" : settings.removeImgClass
+					});
+			});
+			
+			if ( len > (settings.maxFields-1) ) {
+				$("div#" + settings.addContainerId).addClass(settings.hideClass);
+			} else {
+				$("div#" + settings.addContainerId).removeClass(settings.hideClass);
+			}
+		};
+		
+		// handle field removal
+		$("img." + settings.removeImgClass).live("click",function() {
+			// remove the selected row
+			$(this).parent("div." + settings.rowContainerClass).remove();
+			
+			// rebrand the remaining fields sequentially
+			getFields().each(function(i,elem) {
+				var pos = new Number(i+1);
+				var d = $(elem)
+							.attr("id",settings.cloneContainerId + "-" + pos);
+				
+				d.children("label")
+							.attr("for",settings.baseName + pos)
+							.html((pos > 1) ? "" : settings.labelText);
+				
+				d.children("input")
+							.attr({
+								"id":settings.baseName + pos,
+								"name":settings.baseName + pos
+							});
+			});
+			
+			addRemoveBtnCk();
+		});
+		
+		// handle field add
+		$("div#" + settings.addContainerId + " span." + settings.addTriggerClass).click(function() {
+			var len = getFields().length;
+			var pos = new Number(len+1);
+			var newDiv = f
+							.clone()
+							.attr("id",settings.cloneContainerId + "-" + pos)
+							.addClass(settings.rowContainerClass);
+			
+			newDiv.children("label")
+							.attr("for",settings.baseName + pos)
+							.html("");
+			
+			newDiv.children("input")
+							.attr({
+								"id":settings.baseName + pos,
+								"name":settings.baseName + pos,
+								"value":""
+							});
+			
+			newDiv.children("img")
+							.attr("src",settings.removeImgSrc);
+			
+			if ( len > 0 ) {
+				$("div#" + settings.cloneContainerId + "-" + len).after(newDiv);
+			} else {
+				$("div#" + settings.addContainerId).before(newDiv);
+			}
+			
+			addRemoveBtnCk();
+		});
+	};
+})(jQuery);
diff --git a/ui/static/spacer.gif b/ui/static/spacer.gif
new file mode 100755
index 0000000..5bfd67a
Binary files /dev/null and b/ui/static/spacer.gif differ
