summaryrefslogtreecommitdiff
path: root/meetingtools
diff options
context:
space:
mode:
Diffstat (limited to 'meetingtools')
-rw-r--r--meetingtools/__init__.py0
-rw-r--r--meetingtools/ac/__init__.py57
-rw-r--r--meetingtools/ac/api.py214
-rw-r--r--meetingtools/apps/__init__.py0
-rw-r--r--meetingtools/apps/auth/__init__.py79
-rw-r--r--meetingtools/apps/auth/utils.py19
-rw-r--r--meetingtools/apps/auth/views.py169
-rw-r--r--meetingtools/apps/cluster/__init__.py0
-rw-r--r--meetingtools/apps/cluster/admin.py10
-rw-r--r--meetingtools/apps/cluster/migrations/0001_initial.py45
-rw-r--r--meetingtools/apps/cluster/migrations/__init__.py0
-rw-r--r--meetingtools/apps/cluster/models.py35
-rw-r--r--meetingtools/apps/room/__init__.py0
-rw-r--r--meetingtools/apps/room/admin.py10
-rw-r--r--meetingtools/apps/room/feeds.py118
-rw-r--r--meetingtools/apps/room/forms.py82
-rw-r--r--meetingtools/apps/room/management/__init__.py1
-rw-r--r--meetingtools/apps/room/management/commands/__init__.py1
-rw-r--r--meetingtools/apps/room/management/commands/import_rooms.py11
-rw-r--r--meetingtools/apps/room/migrations/0001_initial.py120
-rw-r--r--meetingtools/apps/room/migrations/0002_auto__add_field_room_deleted_sco_id.py91
-rw-r--r--meetingtools/apps/room/migrations/__init__.py0
-rw-r--r--meetingtools/apps/room/models.py142
-rw-r--r--meetingtools/apps/room/tasks.py250
-rw-r--r--meetingtools/apps/room/views.py548
-rw-r--r--meetingtools/apps/stats/__init__.py0
-rw-r--r--meetingtools/apps/stats/forms.py11
-rw-r--r--meetingtools/apps/stats/views.py273
-rw-r--r--meetingtools/apps/userprofile/__init__.py0
-rw-r--r--meetingtools/apps/userprofile/admin.py4
-rw-r--r--meetingtools/apps/userprofile/models.py21
-rw-r--r--meetingtools/context_processors.py23
-rw-r--r--meetingtools/django-crossdomainxhr-middleware.py44
-rw-r--r--meetingtools/extensions/__init__.py0
-rw-r--r--meetingtools/extensions/templatetags/__init__.py0
-rw-r--r--meetingtools/extensions/templatetags/datehumanize.py36
-rw-r--r--meetingtools/extensions/templatetags/roomurl.py19
-rw-r--r--meetingtools/manage.py11
-rw-r--r--meetingtools/mimeparse.py123
-rw-r--r--meetingtools/multiresponse.py77
-rw-r--r--meetingtools/settings.py147
-rw-r--r--meetingtools/site_logging.py10
-rw-r--r--meetingtools/urlmiddleware.py126
-rw-r--r--meetingtools/urls.py53
-rw-r--r--meetingtools/utils.py20
45 files changed, 3000 insertions, 0 deletions
diff --git a/meetingtools/__init__.py b/meetingtools/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/meetingtools/__init__.py
diff --git a/meetingtools/ac/__init__.py b/meetingtools/ac/__init__.py
new file mode 100644
index 0000000..2bbd25f
--- /dev/null
+++ b/meetingtools/ac/__init__.py
@@ -0,0 +1,57 @@
+from meetingtools.ac.api import ACPClient
+import time
+from meetingtools.apps.cluster.models import acc_for_user
+from django.core.cache import cache
+from Queue import Queue
+import logging
+from django.contrib.auth.models import User
+
+_pools = {}
+
+MAXCALLS = 10
+MAXIDLE = 10
+
+class ClientPool(object):
+
+ def __init__(self,acc,maxsize=0,increment=2):
+ self._q = Queue(maxsize)
+ self._acc = acc
+ self._increment = increment
+
+ def allocate(self):
+ now = time.time()
+ api = None
+ while not api:
+ if self._q.empty():
+ for i in range(1,self._increment):
+ logging.debug("adding instance %d" % i)
+ api = ACPClient(self._acc.api_url,self._acc.user,self._acc.password,cpool=self)
+ self._q.put_nowait(api)
+
+ api = self._q.get()
+ if api and (api.age > MAXCALLS or now - api.lastused > MAXIDLE):
+ api = None
+ return api
+
+# with ac_api_client(acc) as api
+# ...
+
+def ac_api_client(o):
+ acc = o
+ logging.debug("ac_api_client(%s)" % repr(o))
+ if hasattr(o,'user') and isinstance(getattr(o,'user'),User):
+ acc = acc_for_user(getattr(o,'user'))
+ elif hasattr(o,'acc'):
+ acc = getattr(o,'acc')
+
+ tag = 'ac_api_client_%d' % acc.id
+ pool = _pools.get(tag)
+ if pool is None:
+ pool = ClientPool(acc,maxsize=30)
+ _pools[tag] = pool
+
+ return pool.allocate()
+
+
+
+ \ No newline at end of file
diff --git a/meetingtools/ac/api.py b/meetingtools/ac/api.py
new file mode 100644
index 0000000..5fbcfbf
--- /dev/null
+++ b/meetingtools/ac/api.py
@@ -0,0 +1,214 @@
+'''
+Created on Jan 31, 2011
+
+@author: leifj
+'''
+from StringIO import StringIO
+
+import httplib2
+from urllib import quote_plus
+import logging
+from pprint import pformat
+import os
+import tempfile
+import time
+from lxml import etree
+from meetingtools.site_logging import logger
+import lxml
+from django.http import HttpResponseRedirect
+from celery.execute import send_task
+
+class ACPException(Exception):
+ def __init__(self, value):
+ self.value = value
+
+ def __str__(self):
+ return etree.tostring(self.value)
+
+def _first_or_none(x):
+ if not x:
+ return None
+ return x[0]
+
+class ACPResult():
+
+ def __init__(self,content):
+ self.et = etree.fromstring(content)
+ self.status = _first_or_none(self.et.xpath('//status'))
+
+ def is_error(self):
+ return self.status_code() != 'ok'
+
+ def status_code(self):
+ return self.status.get('code')
+
+ def subcode(self):
+ return self.status.get('subcode')
+
+ def exception(self):
+ raise ACPException,self.status
+
+ def get_principal(self):
+ logger.debug(lxml.etree.tostring(self.et))
+ return _first_or_none(self.et.xpath('//principal'))
+
+def _enc(v):
+ ev = v
+ if isinstance(ev,str) or isinstance(ev,unicode):
+ ev = ev.encode('iso-8859-1')
+ return ev
+
+def _getset(d,key,value=None):
+ if value:
+ if d.has_key(key):
+ return d[key]
+ else:
+ return None
+ else:
+ d[key] = value
+
+class ACPClient():
+
+ def __init__(self,url,username=None,password=None,cache=True,cpool=None):
+ self._cpool = cpool
+ self.age = 0
+ self.createtime = time.time()
+ self.lastused = self.createtime
+ self.url = url
+ self.session = None
+ if username and password:
+ self.login(username,password)
+ if cache:
+ self._cache = {'login':{},'group':{}}
+
+ def __exit__(self,type,value,traceback):
+ if self._cpool and not value:
+ self._cpool._q.put_nowait(self)
+
+ def __enter__(self):
+ return self
+
+
+ def request(self,method,p={},raise_error=False):
+ self.age += 1
+ self.lastused = time.time()
+ u = list()
+ u.append("action=%s" % method)
+ if self.session:
+ u.append("session=%s" % self.session)
+ for k,v in p.items():
+ value = v
+ if type(v) == int:
+ value = "%d" % value
+ u.append('%s=%s' % (k,quote_plus(value.encode("utf-8"))))
+
+ url = self.url + '?' + '&'.join(u)
+
+ h = httplib2.Http(tempfile.gettempdir()+os.sep+".cache",disable_ssl_certificate_validation=True);
+ logging.debug(url)
+ resp, content = h.request(url, "GET")
+ logging.debug(pformat(resp))
+ logging.debug(pformat(content))
+ if resp.status != 200:
+ raise ACPException,resp.reason
+
+ if resp.has_key('set-cookie'):
+ cookie = resp['set-cookie']
+ if cookie:
+ avp = cookie.split(";")
+ if len(avp) > 0:
+ av = avp[0].split('=')
+ self.session = av[1]
+
+ r = ACPResult(content)
+ if r.is_error() and raise_error:
+ raise r.exception()
+
+ return r;
+
+ def redirect_to(self,url):
+ if self.session:
+ return HttpResponseRedirect("%s?session=%s" % (url,self.session))
+ else:
+ return HttpResponseRedirect(url)
+
+ def login(self,username,password):
+ result = self.request('login',{'login':username,'password':password})
+ if result.is_error():
+ raise result.exception()
+ return result
+
+ def find_or_create_principal(self,key,value,t,d):
+ if not self._cache.has_key(t):
+ self._cache[t] = {}
+ cache = self._cache[t]
+
+ # lxml etree Elements are not picklable
+ p = None
+ if not cache.has_key(key):
+ p = self._find_or_create_principal(key,value,t,d)
+ cache[key] = etree.tostring(p)
+ else:
+ p = etree.parse(StringIO(cache[key]))
+ return p
+
+ def find_principal(self,key,value,t):
+ return self.find_or_create_principal(key,value,t,None)
+
+ def _find_or_create_principal(self,key,value,t,d):
+ result = self.request('principal-list',{'filter-%s' % key: value,'filter-type': t}, True)
+ principal = result.get_principal()
+ if result.is_error():
+ if result.status_code() != 'no_data':
+ result.exception()
+ elif principal and d:
+ d['principal-id'] = principal.get('principal-id')
+
+ rp = principal
+ if d:
+ update_result = self.request('principal-update',d)
+ rp = update_result.get_principal()
+ if not rp:
+ rp = principal
+ return rp
+
+ def find_builtin(self,t):
+ result = self.request('principal-list', {'filter-type': t}, True)
+ return result.get_principal()
+
+ def find_group(self,name):
+ result = self.request('principal-list',{'filter-name':name,'filter-type':'group'},True)
+ return result.get_principal()
+
+ def find_user(self,login):
+ return self.find_principal("login", login, "user")
+
+ def add_remove_member(self,principal_id,group_id,is_member):
+ m = "0"
+ if is_member:
+ m = "1"
+ self.request('group-membership-update',{'group-id': group_id, 'principal-id': principal_id,'is-member':m},True)
+
+ def add_member(self,principal_id,group_id):
+ return self.add_remove_member(principal_id, group_id, True)
+
+ def remove_member(self,principal_id,group_id):
+ return self.add_remove_member(principal_id, group_id, False)
+
+ def user_counts(self,sco_id):
+ user_count = None
+ host_count = None
+ userlist = self.request('meeting-usermanager-user-list',{'sco-id': sco_id},False)
+ if userlist.status_code() == 'ok':
+ user_count = int(userlist.et.xpath("count(.//userdetails)"))
+ host_count = int(userlist.et.xpath("count(.//userdetails/role[text() = 'host'])"))
+ elif userlist.status_code() == 'no-access' and userlist.subcode() == 'not-available': #no active session
+ user_count = 0
+ host_count = 0
+
+ return (user_count,host_count)
+
+ def poll_user_counts(self,room):
+ (room.user_count,room.host_count) = self.user_counts(room.sco_id)
+ room.save()
+ return (room.user_count,room.host_count) \ No newline at end of file
diff --git a/meetingtools/apps/__init__.py b/meetingtools/apps/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/meetingtools/apps/__init__.py
diff --git a/meetingtools/apps/auth/__init__.py b/meetingtools/apps/auth/__init__.py
new file mode 100644
index 0000000..e69cc29
--- /dev/null
+++ b/meetingtools/apps/auth/__init__.py
@@ -0,0 +1,79 @@
+__author__ = 'leifj'
+
+from django.conf import settings
+from saml2.config import SPConfig
+import copy
+from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT
+
+import logging
+logging.basicConfig()
+logger = logging.getLogger("djangosaml2")
+logger.setLevel(logging.DEBUG)
+
+def asgard_sp_config(request=None):
+ host = "localhost"
+ if request is not None:
+ host = request.get_host().replace(":","-")
+ x= {
+ # your entity id, usually your subdomain plus the url to the metadata view
+ 'entityid': 'https://%s/saml2/sp/metadata' % host,
+ # directory with attribute mapping
+ "attribute_map_dir" : "%s/saml2/attributemaps" % settings.BASE_DIR,
+ # this block states what services we provide
+ 'service': {
+ # we are just a lonely SP
+ 'sp' : {
+ 'name': 'meetingtools',
+ 'endpoints': {
+ # url and binding to the assertion consumer service view
+ # do not change the binding osettingsr service name
+ 'assertion_consumer_service': [
+ ('https://%s/saml2/sp/acs/' % host,
+ BINDING_HTTP_POST),
+ ],
+ # url and binding to the single logout service view
+ # do not change the binding or service name
+ 'single_logout_service': [
+ ('https://%s/saml2/sp/ls/' % host,
+ BINDING_HTTP_REDIRECT),
+ ],
+ },
+ # attributes that this project need to identify a user
+ 'required_attributes': ['eduPersonPrincipalName','displayName','eduPersonScopedAffiliation'],
+ }
+ },
+
+ # where the remote metadata is stored
+ #'metadata': { 'remote': [{'url':'http://md.swamid.se/md/swamid-idp.xml',
+ # 'cert':'%s/saml2/credentials/md-signer.crt' % settings.BASE_DIR}] },
+ 'metadata': {'local': [settings.SAML_METADATA_FILE]},
+
+ # set to 1 to output debugging information
+ 'debug': 1,
+
+ # certificate
+ "key_file" : "%s/%s.key" % (settings.SSL_KEY_DIR,host),
+ "cert_file" : "%s/%s.crt" % (settings.SSL_CRT_DIR,host),
+ # own metadata settings
+ 'contact_person': [
+ {'given_name': 'Leif',
+ 'sur_name': 'Johansson',
+ 'company': 'NORDUnet',
+ 'email_address': 'leifj@nordu.net',
+ 'contact_type': 'technical'},
+ {'given_name': 'Johan',
+ 'sur_name': 'Berggren',
+ 'company': 'NORDUnet',
+ 'email_address': 'jbn@nordu.net',
+ 'contact_type': 'technical'},
+ ],
+ # you can set multilanguage information here
+ 'organization': {
+ 'name': [('NORDUNet', 'en')],
+ 'display_name': [('NORDUnet A/S', 'en')],
+ 'url': [('http://www.nordu.net', 'en')],
+ }
+ }
+ c = SPConfig()
+ c.load(copy.deepcopy(x))
+ return c
diff --git a/meetingtools/apps/auth/utils.py b/meetingtools/apps/auth/utils.py
new file mode 100644
index 0000000..1a0174c
--- /dev/null
+++ b/meetingtools/apps/auth/utils.py
@@ -0,0 +1,19 @@
+'''
+Created on Jul 7, 2010
+
+@author: leifj
+'''
+from uuid import uuid4
+
+def nonce():
+ return uuid4().hex
+
+def anonid():
+ return uuid4().urn
+
+def groups(request):
+ groups = []
+ if request.user.is_authenticated():
+ groups = request.user.groups
+
+ return groups \ No newline at end of file
diff --git a/meetingtools/apps/auth/views.py b/meetingtools/apps/auth/views.py
new file mode 100644
index 0000000..ee23df3
--- /dev/null
+++ b/meetingtools/apps/auth/views.py
@@ -0,0 +1,169 @@
+'''
+Created on Jul 5, 2010
+
+@author: leifj
+'''
+from django.http import HttpResponseRedirect
+from django.contrib.auth.models import User, Group
+import datetime
+from django.views.decorators.cache import never_cache
+import logging
+from meetingtools.apps.userprofile.models import UserProfile
+from meetingtools.multiresponse import redirect_to, make_response_dict
+from meetingtools.ac import ac_api_client
+from django.shortcuts import render_to_response
+from django.contrib import auth
+from django_co_connector.models import co_import_from_request, add_member,remove_member
+from meetingtools.apps.cluster.models import acc_for_user
+from django.conf import settings
+
+def meta(request,attr):
+ v = request.META.get(attr)
+ if not v:
+ return None
+ values = filter(lambda x: x != "(null)",v.split(";"))
+ return values;
+
+def meta1(request,attr):
+ v = meta(request,attr)
+ if v:
+ return str(v[0]).decode('utf-8')
+ else:
+ return None
+
+def _localpart(a):
+ if hasattr(a,'name'):
+ a = a.name
+ if '@' in a:
+ (lp,dp) = a.split('@')
+ a = lp
+ return a
+
+def _is_member_or_employee_old(affiliations):
+ lpa = map(_localpart,affiliations)
+ return 'student' in lpa or 'staff' in lpa or ('member' in lpa and not 'student' in lpa)
+
+def _is_member_or_employee(user):
+ lpa = map(_localpart,user.groups.all())
+ return 'student' in lpa or 'staff' in lpa or ('member' in lpa and not 'student' in lpa)
+
+@never_cache
+def logout(request):
+ auth.logout(request)
+ return HttpResponseRedirect('/Shibboleth.sso/Logout')
+
+@never_cache
+def login(request):
+ return render_to_response('apps/auth/login.html',make_response_dict(request,{'next': request.REQUEST.get("next")}));
+
+def join_group(group,**kwargs):
+ user = kwargs['user']
+ acc = acc_for_user(user)
+ with ac_api_client(acc) as api:
+ principal = api.find_principal("login", user.username, "user")
+ if principal:
+ gp = api.find_group(group.name)
+ if gp:
+ api.add_member(principal.get('principal-id'),gp.get('principal-id'))
+
+def leave_group(group,**kwargs):
+ user = kwargs['user']
+ acc = acc_for_user(user)
+ with ac_api_client(acc) as api:
+ principal = api.find_principal("login", user.username, "user")
+ if principal:
+ gp = api.find_group(group.name)
+ if gp:
+ api.remove_member(principal.get('principal-id'),gp.get('principal-id'))
+
+add_member.connect(join_group,sender=Group)
+remove_member.connect(leave_group,sender=Group)
+
+def accounts_login_federated(request):
+ if request.user.is_authenticated():
+ profile,created = UserProfile.objects.get_or_create(user=request.user)
+ if created:
+ profile.identifier = request.user.username
+ profile.user = request.user
+ profile.save()
+
+ update = False
+ fn = meta1(request,'givenName')
+ ln = meta1(request,'sn')
+ cn = meta1(request,'cn')
+ if not cn:
+ cn = meta1(request,'displayName')
+ logging.debug("cn=%s" % cn)
+ if not cn and fn and ln:
+ cn = "%s %s" % (fn,ln)
+ if not cn:
+ cn = profile.identifier
+
+ mail = meta1(request,'mail')
+
+ idp = meta1(request,'Shib-Identity-Provider')
+
+ for attrib_name, meta_value in (('display_name',cn),('email',mail),('idp',idp)):
+ attrib_value = getattr(profile, attrib_name)
+ if meta_value and not attrib_value:
+ setattr(profile,attrib_name,meta_value)
+ update = True
+
+ if request.user.password == "":
+ request.user.password = "(not used for federated logins)"
+ update = True
+
+ if update:
+ request.user.save()
+
+ # Allow auto_now to kick in for the lastupdated field
+ #profile.lastupdated = datetime.datetime.now()
+ profile.save()
+
+ next = request.session.get("after_login_redirect", None)
+ if not next and request.GET.has_key('next'):
+ next = request.GET['next']
+ else:
+ next = settings.DEFAULT_URL
+
+ acc = acc_for_user(request.user)
+ with ac_api_client(request) as api:
+ # make sure the principal is created before shooting off
+ principal = api.find_or_create_principal("login", request.user.username, "user",
+ {'type': "user",
+ 'has-children': "0",
+ 'first-name':fn,
+ 'last-name':ln,
+ 'email':mail,
+ 'send-email': 0,
+ 'login':request.user.username,
+ 'ext-login':request.user.username})
+
+
+
+ co_import_from_request(request)
+
+ member_or_employee = _is_member_or_employee(request.user)
+ for gn in ('live-admins','seminar-admins'):
+ group = api.find_builtin(gn)
+ if group:
+ api.add_remove_member(principal.get('principal-id'),group.get('principal-id'),member_or_employee)
+
+ #(lp,domain) = uid.split('@')
+ #for a in ('student','employee','member'):
+ # affiliation = "%s@%s" % (a,domain)
+ # group = connect_api.find_or_create_principal('name',affiliation,'group',{'type': 'group','has-children':'1','name': affiliation})
+ # member = affiliation in affiliations
+ # connect_api.add_remove_member(principal.get('principal-id'),group.get('principal-id'),member)
+
+ #for e in epe:
+ # group = connect_api.find_or_create_principal('name',e,'group',{'type': 'group','has-children':'1','name': e})
+ # if group:
+ # connect_api.add_remove_member(principal.get('principal-id'),group.get('principal-id'),True)
+
+ if next is not None:
+ return redirect_to(next)
+ else:
+ pass
+
+ return redirect_to(settings.LOGIN_URL)
diff --git a/meetingtools/apps/cluster/__init__.py b/meetingtools/apps/cluster/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/meetingtools/apps/cluster/__init__.py
diff --git a/meetingtools/apps/cluster/admin.py b/meetingtools/apps/cluster/admin.py
new file mode 100644
index 0000000..3fc9eea
--- /dev/null
+++ b/meetingtools/apps/cluster/admin.py
@@ -0,0 +1,10 @@
+'''
+Created on Jan 31, 2011
+
+@author: leifj
+'''
+
+from django.contrib import admin
+from meetingtools.apps.cluster.models import ACCluster
+
+admin.site.register(ACCluster) \ No newline at end of file
diff --git a/meetingtools/apps/cluster/migrations/0001_initial.py b/meetingtools/apps/cluster/migrations/0001_initial.py
new file mode 100644
index 0000000..f20f743
--- /dev/null
+++ b/meetingtools/apps/cluster/migrations/0001_initial.py
@@ -0,0 +1,45 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding model 'ACCluster'
+ db.create_table('cluster_accluster', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('api_url', self.gf('django.db.models.fields.URLField')(max_length=200)),
+ ('url', self.gf('django.db.models.fields.URLField')(max_length=200)),
+ ('user', self.gf('django.db.models.fields.CharField')(max_length=128)),
+ ('password', self.gf('django.db.models.fields.CharField')(max_length=128)),
+ ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=128, blank=True)),
+ ('default_template_sco_id', self.gf('django.db.models.fields.IntegerField')(unique=True, blank=True)),
+ ('domain_match', self.gf('django.db.models.fields.TextField')()),
+ ))
+ db.send_create_signal('cluster', ['ACCluster'])
+
+
+ def backwards(self, orm):
+
+ # Deleting model 'ACCluster'
+ db.delete_table('cluster_accluster')
+
+
+ models = {
+ 'cluster.accluster': {
+ 'Meta': {'object_name': 'ACCluster'},
+ 'api_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
+ 'default_template_sco_id': ('django.db.models.fields.IntegerField', [], {'unique': 'True', 'blank': 'True'}),
+ 'domain_match': ('django.db.models.fields.TextField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
+ 'user': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+ }
+ }
+
+ complete_apps = ['cluster']
diff --git a/meetingtools/apps/cluster/migrations/__init__.py b/meetingtools/apps/cluster/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/meetingtools/apps/cluster/migrations/__init__.py
diff --git a/meetingtools/apps/cluster/models.py b/meetingtools/apps/cluster/models.py
new file mode 100644
index 0000000..3c65d57
--- /dev/null
+++ b/meetingtools/apps/cluster/models.py
@@ -0,0 +1,35 @@
+'''
+Created on Feb 3, 2011
+
+@author: leifj
+'''
+
+from django.db import models
+from django.db.models.fields import CharField, URLField, TextField, IntegerField
+import re
+
+class ACCluster(models.Model):
+ api_url = URLField()
+ url = URLField()
+ user = CharField(max_length=128)
+ password = CharField(max_length=128)
+ name = CharField(max_length=128,blank=True,unique=True)
+ default_template_sco_id = IntegerField(blank=True,unique=True)
+ domain_match = TextField()
+
+ def __unicode__(self):
+ return self.url
+
+ def make_url(self,path=""):
+ return "%s%s" % (self.url,path)
+
+def acc_for_user(user):
+ (local,domain) = user.username.split('@')
+ if not domain:
+ #raise Exception,"Improperly formatted user: %s" % user.username
+ domain = "nordu.net" # testing with local accts only
+ for acc in ACCluster.objects.all():
+ for regex in acc.domain_match.split():
+ if re.match(regex.strip(),domain):
+ return acc
+ raise Exception,"I don't know which cluster you belong to... (%s)" % user.username
diff --git a/meetingtools/apps/room/__init__.py b/meetingtools/apps/room/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/meetingtools/apps/room/__init__.py
diff --git a/meetingtools/apps/room/admin.py b/meetingtools/apps/room/admin.py
new file mode 100644
index 0000000..13d80a8
--- /dev/null
+++ b/meetingtools/apps/room/admin.py
@@ -0,0 +1,10 @@
+'''
+Created on Jan 31, 2011
+
+@author: leifj
+'''
+
+from django.contrib import admin
+from meetingtools.apps.room.models import Room
+
+admin.site.register(Room) \ No newline at end of file
diff --git a/meetingtools/apps/room/feeds.py b/meetingtools/apps/room/feeds.py
new file mode 100644
index 0000000..a72caaa
--- /dev/null
+++ b/meetingtools/apps/room/feeds.py
@@ -0,0 +1,118 @@
+'''
+Created on May 13, 2011
+
+@author: leifj
+'''
+
+from django.contrib.syndication.views import Feed
+from meetingtools.apps.room.models import Room
+from tagging.models import TaggedItem
+from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed
+from meetingtools.apps.room.views import room_recordings
+from django.shortcuts import get_object_or_404
+
+class TagsWrapper(object):
+
+ def __init__(self,tn):
+ self.tags = tn.split('+')
+ self.rooms = TaggedItem.objects.get_by_model(Room, tn.split('+'))
+
+ def title(self):
+ return "Rooms tagged with %s" % " and ".join(self.tags)
+
+ def description(self):
+ return self.title()
+
+ def link(self,ext):
+ return "/room/+%s.%s" % ("+".join(self.tags),ext)
+
+class MeetingToolsFeed(Feed):
+
+ item_author_name = 'SUNET e-meeting tools'
+
+ def ext(self):
+ if self.feed_type == Atom1Feed:
+ return "atom"
+
+ if self.feed_type == Rss201rev2Feed:
+ return "rss"
+
+ return "rss"
+
+
+class RoomTagFeed(MeetingToolsFeed):
+
+ def get_object(self,request,tn):
+ return TagsWrapper(tn)
+
+ def title(self,t):
+ return t.title()
+
+ def link(self,t):
+ return t.link(self.ext())
+
+ def description(self,t):
+ return t.description()
+
+ def items(self,t):
+ return t.rooms
+
+ def item_title(self,room):
+ return room.name
+
+ def item_description(self,room):
+ return room.description
+
+ def item_link(self,room):
+ return room.go_url()
+
+ def item_guid(self,room):
+ return room.permalink()
+
+ def item_pubdate(self,room):
+ return room.lastupdated
+
+
+class RoomAtomTagFeed(RoomTagFeed):
+ feed_type = Atom1Feed
+
+class RoomRSSTagField(RoomTagFeed):
+ feed_type = Rss201rev2Feed
+
+class RecordingsWrapper(object):
+ def __init__(self,room,request):
+ self.room = room
+ self.items = room_recordings(request, room)
+
+class RecordingFeed(MeetingToolsFeed):
+
+ def get_object(self,request,rid):
+ room = get_object_or_404(Room,pk=rid)
+ return RecordingsWrapper(room,request)
+
+ def title(self,recordings):
+ return "Recordings in room '%s'" % recordings.room.name
+
+ def link(self,recordings):
+ return recordings.room.recordings_url()
+
+ def items(self,recordings):
+ return recordings.items
+
+ def item_title(self,recording):
+ return recording['name']
+
+ def item_description(self,recording):
+ return recording['description']
+
+ def item_link(self,recording):
+ return recording['url']
+
+ def item_pubdate(self,recording):
+ return recording['date_created']
+
+class AtomRecordingFeed(RecordingFeed):
+ feed_type = Atom1Feed
+
+class RSSRecordingField(RecordingFeed):
+ feed_type = Rss201rev2Feed \ No newline at end of file
diff --git a/meetingtools/apps/room/forms.py b/meetingtools/apps/room/forms.py
new file mode 100644
index 0000000..62b515b
--- /dev/null
+++ b/meetingtools/apps/room/forms.py
@@ -0,0 +1,82 @@
+'''
+Created on Feb 1, 2011
+
+@author: leifj
+'''
+
+from meetingtools.apps.room.models import Room
+from django.forms.widgets import Select, TextInput, RadioSelect, Textarea
+from django.forms.fields import BooleanField, ChoiceField, CharField
+from django.forms.forms import Form
+from form_utils.forms import BetterModelForm
+from django.utils.safestring import mark_safe
+from django.forms.models import ModelForm
+
+PUBLIC = 0
+PROTECTED = 1
+PRIVATE = 2
+
+class PrefixTextInput(TextInput):
+ def __init__(self, attrs=None, prefix=None):
+ super(PrefixTextInput, self).__init__(attrs)
+ self.prefix = prefix
+
+ def render(self, name, value, attrs=None):
+ return mark_safe("<div class=\"input-prepend\"><span class=\"add-on\">"+self.prefix+"</span>"+
+ super(PrefixTextInput, self).render(name, value, attrs)+"</span></div>")
+
+class ModifyRoomForm(ModelForm):
+ class Meta:
+ model = Room
+ fields = ['name','description','source_sco_id','self_cleaning','allow_host']
+ widgets = {'source_sco_id': Select(),
+ 'description': Textarea(attrs={'rows': 4, 'cols': 50}),
+ 'name': TextInput(attrs={'size': '40'})}
+
+
+class CreateRoomForm(BetterModelForm):
+
+ access = ChoiceField(choices=(('public','Public'),('private','Private')))
+
+ class Meta:
+ model = Room
+ fields = ['name','description','urlpath','access','self_cleaning','allow_host']
+ fieldsets = [('name',{'fields': ['name'],
+ 'classes': ['step'],
+ 'legend': 'Step 1: Room name',
+ 'description': 'The room name should be short and descriptive.'
+ }),
+ ('description',{'fields': ['description'],
+ 'classes': ['step'],
+ 'legend': 'Step 2: Room description',
+ 'description': 'Please provide a short summary of this room.'
+ }),
+ ('properties',{'fields': ['self_cleaning','allow_host','urlpath','access'],
+ 'classes': ['step'],
+ 'legend': 'Step 3: Room properties',
+ 'description': '''
+ <p>These are basic properties for your room. If you set your room to cleaned up after
+ use it will be reset every time the last participant leaves the room. If you create a public room it
+ will be open to anyone who has the room URL. If you create a private room then guests will have to be
+ approved by an active meeting host before being able to join the room.</p>
+
+ <div class="ui-widget">
+ <div class="ui-state-highlight ui-corner-all" style="margin-top: 20px; padding: 0 .7em;">
+ <p><span class="ui-icon ui-icon-info" style="float: left; margin-right: .3em;"></span>
+ <strong>Warning</strong> Setting a room to be cleaned up when empty will cause all existing content
+ associated with the to be destroyed each time the room is reset.</p>
+ </div>
+ </div>
+ '''
+ }),
+ ]
+ widgets = {'access': RadioSelect(),
+ 'urlpath': PrefixTextInput(attrs={'size': '10'}),
+ 'description': Textarea(attrs={'rows': 4, 'cols': 50}),
+ 'name': TextInput(attrs={'size': '40'})}
+
+class DeleteRoomForm(Form):
+ confirm = BooleanField(label="Confirm remove room")
+
+class TagRoomForm(Form):
+ tag = CharField(max_length=256) \ No newline at end of file
diff --git a/meetingtools/apps/room/management/__init__.py b/meetingtools/apps/room/management/__init__.py
new file mode 100644
index 0000000..3929ed7
--- /dev/null
+++ b/meetingtools/apps/room/management/__init__.py
@@ -0,0 +1 @@
+__author__ = 'leifj'
diff --git a/meetingtools/apps/room/management/commands/__init__.py b/meetingtools/apps/room/management/commands/__init__.py
new file mode 100644
index 0000000..3929ed7
--- /dev/null
+++ b/meetingtools/apps/room/management/commands/__init__.py
@@ -0,0 +1 @@
+__author__ = 'leifj'
diff --git a/meetingtools/apps/room/management/commands/import_rooms.py b/meetingtools/apps/room/management/commands/import_rooms.py
new file mode 100644
index 0000000..7944be8
--- /dev/null
+++ b/meetingtools/apps/room/management/commands/import_rooms.py
@@ -0,0 +1,11 @@
+from django.core.management import BaseCommand
+from meetingtools.apps.cluster.models import ACCluster
+from meetingtools.apps.room.tasks import import_acc
+
+__author__ = 'leifj'
+
+class Command(BaseCommand):
+
+ def handle(self, *args, **options):
+ for acc in ACCluster.objects.all():
+ import_acc(acc,since=0) \ No newline at end of file
diff --git a/meetingtools/apps/room/migrations/0001_initial.py b/meetingtools/apps/room/migrations/0001_initial.py
new file mode 100644
index 0000000..4cb1bef
--- /dev/null
+++ b/meetingtools/apps/room/migrations/0001_initial.py
@@ -0,0 +1,120 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding model 'Room'
+ db.create_table('room_room', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('creator', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=128)),
+ ('urlpath', self.gf('django.db.models.fields.CharField')(unique=True, max_length=128)),
+ ('acc', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['cluster.ACCluster'])),
+ ('self_cleaning', self.gf('django.db.models.fields.BooleanField')(default=False)),
+ ('allow_host', self.gf('django.db.models.fields.BooleanField')(default=True)),
+ ('sco_id', self.gf('django.db.models.fields.IntegerField')()),
+ ('source_sco_id', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)),
+ ('folder_sco_id', self.gf('django.db.models.fields.IntegerField')()),
+ ('description', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
+ ('user_count', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)),
+ ('host_count', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)),
+ ('timecreated', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
+ ('lastupdated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
+ ('lastvisited', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)),
+ ))
+ db.send_create_signal('room', ['Room'])
+
+ # Adding unique constraint on 'Room', fields ['acc', 'sco_id']
+ db.create_unique('room_room', ['acc_id', 'sco_id'])
+
+ # Adding unique constraint on 'Room', fields ['name', 'folder_sco_id']
+ db.create_unique('room_room', ['name', 'folder_sco_id'])
+
+
+ def backwards(self, orm):
+
+ # Removing unique constraint on 'Room', fields ['name', 'folder_sco_id']
+ db.delete_unique('room_room', ['name', 'folder_sco_id'])
+
+ # Removing unique constraint on 'Room', fields ['acc', 'sco_id']
+ db.delete_unique('room_room', ['acc_id', 'sco_id'])
+
+ # Deleting model 'Room'
+ db.delete_table('room_room')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'cluster.accluster': {
+ 'Meta': {'object_name': 'ACCluster'},
+ 'api_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
+ 'default_template_sco_id': ('django.db.models.fields.IntegerField', [], {'unique': 'True', 'blank': 'True'}),
+ 'domain_match': ('django.db.models.fields.TextField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
+ 'user': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'room.room': {
+ 'Meta': {'unique_together': "(('acc', 'sco_id'), ('name', 'folder_sco_id'))", 'object_name': 'Room'},
+ 'acc': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cluster.ACCluster']"}),
+ 'allow_host': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'folder_sco_id': ('django.db.models.fields.IntegerField', [], {}),
+ 'host_count': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'lastupdated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'lastvisited': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'sco_id': ('django.db.models.fields.IntegerField', [], {}),
+ 'self_cleaning': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'source_sco_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'timecreated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'urlpath': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
+ 'user_count': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'})
+ }
+ }
+
+ complete_apps = ['room']
diff --git a/meetingtools/apps/room/migrations/0002_auto__add_field_room_deleted_sco_id.py b/meetingtools/apps/room/migrations/0002_auto__add_field_room_deleted_sco_id.py
new file mode 100644
index 0000000..bea1f14
--- /dev/null
+++ b/meetingtools/apps/room/migrations/0002_auto__add_field_room_deleted_sco_id.py
@@ -0,0 +1,91 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding field 'Room.deleted_sco_id'
+ db.add_column('room_room', 'deleted_sco_id', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'Room.deleted_sco_id'
+ db.delete_column('room_room', 'deleted_sco_id')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'cluster.accluster': {
+ 'Meta': {'object_name': 'ACCluster'},
+ 'api_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
+ 'default_template_sco_id': ('django.db.models.fields.IntegerField', [], {'unique': 'True', 'blank': 'True'}),
+ 'domain_match': ('django.db.models.fields.TextField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
+ 'user': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'room.room': {
+ 'Meta': {'unique_together': "(('acc', 'sco_id'), ('name', 'folder_sco_id'))", 'object_name': 'Room'},
+ 'acc': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['cluster.ACCluster']"}),
+ 'allow_host': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'deleted_sco_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'folder_sco_id': ('django.db.models.fields.IntegerField', [], {}),
+ 'host_count': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'lastupdated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'lastvisited': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'sco_id': ('django.db.models.fields.IntegerField', [], {}),
+ 'self_cleaning': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'source_sco_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'timecreated': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'urlpath': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
+ 'user_count': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'})
+ }
+ }
+
+ complete_apps = ['room']
diff --git a/meetingtools/apps/room/migrations/__init__.py b/meetingtools/apps/room/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/meetingtools/apps/room/migrations/__init__.py
diff --git a/meetingtools/apps/room/models.py b/meetingtools/apps/room/models.py
new file mode 100644
index 0000000..2177b24
--- /dev/null
+++ b/meetingtools/apps/room/models.py
@@ -0,0 +1,142 @@
+'''
+Created on Jan 31, 2011
+
+@author: leifj
+'''
+
+from django.db import models
+from django.db.models.fields import CharField, BooleanField, IntegerField,\
+ TextField
+from django.db.models.fields.related import ForeignKey
+from django.contrib.auth.models import User
+from meetingtools.apps.cluster.models import ACCluster
+import time
+import tagging
+from meetingtools.settings import LOCK_DIR
+from django.db.models.signals import post_save
+from tagging.models import Tag
+import os
+
+class FileLock(object):
+
+ def __init__(self,obj):
+ self.obj = obj
+
+ def __get__(self):
+ return os.access(LOCK_DIR+os.sep+self.obj.__class__+"_"+self.obj.id+".lock",os.F_OK)
+ def __set__(self,value):
+ if not isinstance(value,bool):
+ raise AttributeError
+ if value:
+ f = open(LOCK_DIR+os.sep+self.obj.__class__+"_"+self.obj.id+".lock")
+ f.close()
+ else:
+ os.remove(LOCK_DIR+os.sep+self.obj.__class__+"_"+self.obj.id+".lock")
+ def __delete__(self):
+ raise AttributeError
+
+class RoomLockedException(Exception):
+ def __init__(self, value):
+ self.value = value
+ def __str__(self):
+ return repr(self.value)
+
+class Room(models.Model):
+ creator = ForeignKey(User,editable=False)
+ name = CharField(max_length=128)
+ urlpath = CharField(verbose_name="Custom URL",max_length=128,unique=True)
+ acc = ForeignKey(ACCluster,verbose_name="Adobe Connect Cluster",editable=False)
+ self_cleaning = BooleanField(verbose_name="Clean-up when empty?")
+ allow_host = BooleanField(verbose_name="Allow first participant to become host?",default=True)
+ sco_id = IntegerField(verbose_name="Adobe Connect Room")
+ source_sco_id = IntegerField(verbose_name="Template",blank=True,null=True)
+ deleted_sco_id = IntegerField(verbose_name="Previous Room ID",editable=False,blank=True,null=True)
+ folder_sco_id = IntegerField(verbose_name="Adobe Connect Room Folder",editable=False)
+ description = TextField(blank=True,null=True)
+ user_count = IntegerField(verbose_name="User Count At Last Visit",editable=False,blank=True,null=True)
+ host_count = IntegerField(verbose_name="Host Count At Last Visit",editable=False,blank=True,null=True)
+ timecreated = models.DateTimeField(auto_now_add=True)
+ lastupdated = models.DateTimeField(auto_now=True)
+ lastvisited = models.DateTimeField(blank=True,null=True)
+
+ class Meta:
+ unique_together = (('acc','sco_id'),('name','folder_sco_id'))
+
+ def __unicode__(self):
+ return "%s (sco_id=%s,source_sco_id=%s,folder_sco_id=%s,urlpath=%s)" % (self.name,self.sco_id,self.source_sco_id,self.folder_sco_id,self.urlpath)
+
+ def _lockf(self):
+ return "%s%sroom-%d.lock" % (LOCK_DIR,os.sep,+self.id)
+
+ def lock(self,msg=None):
+ f = open(self._lockf(),'w')
+ if msg:
+ f.write(msg)
+ f.close()
+
+ def trylock(self,raise_on_locked=True):
+ if self.is_locked():
+ if raise_on_locked:
+ raise RoomLockedException,"room %s is locked" % self.__unicode__()
+ else:
+ return False
+ self.lock() #race!! - must use flock
+ return True
+
+ def unlock(self):
+ os.remove(self._lockf())
+
+ def is_locked(self):
+ os.access(self._lockf(),os.F_OK)
+
+ def lastvisit(self):
+ if not self.lastvisited:
+ return 0
+ else:
+ return int(time.mktime(self.lastvisited.timetuple())*1000)
+
+ def lastupdate(self):
+ if not self.lastupdated:
+ return 0
+ else:
+ return int(time.mktime(self.lastupdated.timetuple()))
+
+ def go_url(self):
+ return "/go/%s" % self.urlpath
+
+ def go_url_internal(self):
+ return "/go/%d" % self.id
+
+ def permalink(self):
+ return "/room/%d" % self.id
+
+ def recordings_url(self):
+ return "/room/%d/recordings" % self.id
+
+ def nusers(self):
+ if self.user_count == None:
+ return "unknown many"
+ else:
+ return self.user_count
+
+ def nhosts(self):
+ if self.host_count == None:
+ return "unknown many"
+ else:
+ return self.host_count
+
+tagging.register(Room)
+
+def _magic_tags(sender,**kwargs):
+ room = kwargs['instance']
+ if room.self_cleaning:
+ Tag.objects.add_tag(room, "cleaning")
+ else:
+ tags = Tag.objects.get_for_object(room)
+ ntags = []
+ for tag in tags:
+ if tag.name != "cleaning":
+ ntags.append(tag.name)
+ Tag.objects.update_tags(room, " ".join(ntags))
+
+post_save.connect(_magic_tags,sender=Room) \ No newline at end of file
diff --git a/meetingtools/apps/room/tasks.py b/meetingtools/apps/room/tasks.py
new file mode 100644
index 0000000..ce9f275
--- /dev/null
+++ b/meetingtools/apps/room/tasks.py
@@ -0,0 +1,250 @@
+'''
+Created on Jan 18, 2012
+
+@author: leifj
+'''
+from celery.task import periodic_task,task
+from celery.schedules import crontab
+from meetingtools.apps.cluster.models import ACCluster
+from meetingtools.ac import ac_api_client
+from meetingtools.apps.room.models import Room
+import iso8601
+from django.contrib.auth.models import User
+from django.core.cache import cache
+from django.core.exceptions import ObjectDoesNotExist
+import logging
+from datetime import datetime,timedelta
+from lxml import etree
+from django.db.models import Q
+from django.contrib.humanize.templatetags import humanize
+from django.conf import settings
+from django.core.mail import send_mail
+
+def _owner_username(api,sco):
+ logging.debug(sco)
+ key = '_sco_owner_%s' % sco.get('sco-id')
+ logging.debug(key)
+ try:
+ if cache.get(key) is None:
+ fid = sco.get('folder-id')
+ if not fid:
+ logging.debug("No folder-id")
+ return None
+
+ folder_id = int(fid)
+ r = api.request('sco-info',{'sco-id':folder_id},False)
+ if r.status_code() == 'no-data':
+ return None
+
+ parent = r.et.xpath("//sco")[0]
+ logging.debug("p %s",repr(parent))
+ logging.debug("r %s",etree.tostring(parent))
+ name = None
+ if parent:
+ logging.debug("parent: %s" % parent)
+ if parent.findtext('name') == 'User Meetings':
+ name = sco.findtext('name')
+ else:
+ name = _owner_username(api,parent)
+
+ cache.set(key,name)
+
+ return cache.get(key)
+ except Exception,e:
+ logging.debug(e)
+ return None
+
+def _extended_info(api,sco_id):
+ r = api.request('sco-info',{'sco-id':sco_id},False)
+ if r.status_code == 'no-data':
+ return None
+ return (r.et,_owner_username(api,r.et.xpath('//sco')[0]))
+
+def _import_one_room(acc,api,row):
+ sco_id = int(row.get('sco-id'))
+ last = iso8601.parse_date(row.findtext("date-modified[0]"))
+ room = None
+
+ try:
+ room = Room.objects.get(acc=acc,deleted_sco_id=sco_id)
+ if room is not None:
+ return # We hit a room in the process of being cleaned - let it simmer until next pass
+ except ObjectDoesNotExist:
+ pass
+ except Exception,e:
+ logging.debug(e)
+ return
+
+ try:
+ logging.debug("finding acc=%s,sco_id=%d in our DB" % (acc,sco_id))
+ room = Room.objects.get(acc=acc,sco_id=sco_id)
+ if room.deleted_sco_id is not None:
+ return # We hit a room in the process of being cleaned - let it simmer until next pass
+ room.trylock()
+ except ObjectDoesNotExist:
+ pass
+
+ last = last.replace(tzinfo=None)
+ lastupdated = None
+ if room:
+ lastupdated = room.lastupdated.replace(tzinfo=None) # make the compare work - very ugly
+
+ #logging.debug("last %s" % last)
+ #logging.debug("lastupdated %s" % lastupdated)
+ if not room or lastupdated < last:
+ (r,username) = _extended_info(api, sco_id)
+ logging.debug("found room owned by %s time for and update" % username)
+ if username is None:
+ return
+
+ logging.debug(etree.tostring(row))
+ logging.debug(etree.tostring(r))
+ urlpath = row.findtext("url[0]").strip("/")
+ name = row.findtext('name[0]')
+ description = row.findtext('description[0]')
+ folder_sco_id = 0
+ source_sco_id = 0
+
+ def _ior0(elt,a,dflt):
+ str = elt.get(a,None)
+ if str is None or not str:
+ return dflt
+ else:
+ return int(str)
+
+
+ for elt in r.findall(".//sco[0]"):
+ folder_sco_id = _ior0(elt,'folder-id',0)
+ source_sco_id = _ior0(elt,'source-sco-id',0)
+
+ logging.debug("urlpath=%s, name=%s, folder_sco_id=%s, source_sco_id=%s" % (urlpath,name,folder_sco_id,source_sco_id))
+
+ if room is None:
+ if folder_sco_id:
+ user,created = User.objects.get_or_create(username=username)
+ if created:
+ user.set_unusable_password()
+ room = Room.objects.create(acc=acc,sco_id=sco_id,creator=user,name=name,description=description,folder_sco_id=folder_sco_id,source_sco_id=source_sco_id,urlpath=urlpath)
+ room.trylock()
+ else:
+ if folder_sco_id:
+ room.folder_sco_id = folder_sco_id
+ room.source_sco_id = source_sco_id
+ room.description = description
+ room.urlpath = urlpath
+
+ if room is not None:
+ room.save()
+ room.unlock()
+ else:
+ if room is not None:
+ room.unlock()
+
+def import_acc(acc,since=0):
+ with ac_api_client(acc) as api:
+ r = None
+ if since > 0:
+ then = datetime.now()-timedelta(seconds=since)
+ then = then.replace(microsecond=0)
+ r = api.request('report-bulk-objects',{'filter-type': 'meeting','filter-gt-date-modified': then.isoformat()})
+ else:
+ r = api.request('report-bulk-objects',{'filter-type': 'meeting'})
+
+ for row in r.et.xpath("//row"):
+ try:
+ _import_one_room(acc,api,row)
+ except Exception,ex:
+ logging.error(ex)
+
+@periodic_task(run_every=crontab(hour="*", minute="*", day_of_week="*"))
+def import_all_rooms():
+ for acc in ACCluster.objects.all():
+ import_acc(acc,since=3600)
+
+def start_user_counts_poll(room,niter):
+ poll_user_counts.apply_async(args=[room],kwargs={'niter': niter})
+
+@task(name='meetingtools.apps.room.tasks.poll_user_counts',rate_limit="10/s")
+def poll_user_counts(room,niter=0):
+ logging.debug("polling user_counts for room %s" % room.name)
+ with ac_api_client(room.acc) as api:
+ (nusers,nhosts) = api.poll_user_counts(room)
+ if nusers > 0:
+ logging.debug("room occupied by %d users and %d hosts, checking again in 20 ..." % (nusers,nhosts))
+ poll_user_counts.apply_async(args=[room],kwargs={'niter': 0},countdown=20)
+ elif niter > 0:
+ logging.debug("room empty, will check again in 5 for %d more times ..." % (niter -1))
+ poll_user_counts.apply_async(args=[room],kwargs={'niter': niter-1},countdown=5)
+ return (nusers,nhosts)
+
+# belts and suspenders - we setup polling for active rooms aswell...
+@periodic_task(run_every=crontab(hour="*", minute="*/5", day_of_week="*"))
+def import_recent_user_counts():
+ for acc in ACCluster.objects.all():
+ with ac_api_client(acc) as api:
+ then = datetime.now()-timedelta(seconds=600)
+ for room in Room.objects.filter((Q(lastupdated__gt=then) | Q(lastvisited__gt=then)) & Q(acc=acc)):
+ api.poll_user_counts(room)
+
+# look for sessions that are newer than the one we know about for a room
+@periodic_task(run_every=crontab(hour="*", minute="*", day_of_week="*"))
+def import_sessions():
+ for room in Room.objects.all():
+ with ac_api_client(room.acc) as api:
+ p = {'sco-id': room.sco_id,'sort-date-created': 'asc'}
+ if room.lastvisited != None:
+ last = room.lastvisited
+ last.replace(microsecond=0)
+ p['filter-gt-date-created'] = last.isoformat()
+ r = api.request('report-meeting-sessions',p)
+ for row in r.et.xpath("//row"):
+ date_created = iso8601.parse_date(row.findtext("date-created"))
+ logging.debug("sco_id=%d lastvisited: %s" % (room.sco_id,date_created))
+ room.lastvisited = date_created
+ room.save()
+ break
+
+#@periodic_task(run_every=crontab(hour="*", minutes="*/5", day_of_week="*"))
+def import_transactions():
+ for acc in ACCluster.objects.all():
+ then = datetime.now() - timedelta(seconds=600)
+ then = then.replace(microsecond=0)
+ with ac_api_client(acc) as api:
+ seen = {}
+ r = api.request('report-bulk-consolidated-transactions',{'filter-type':'meeting','sort-date-created': 'asc','filter-gt-date-created': then.isformat()})
+ for row in r.et.xpath("//row"):
+ sco_id = row.get('sco-id')
+ logging.debug("last session for sco_id=%d" % sco_id)
+ if not seen.get(sco_id,False): #pick the first session for each room - ie the one last created
+ seen[sco_id] = True
+ try:
+ room = Room.objects.get(acc=acc,sco_id=sco_id)
+ date_created = iso8601.parse_date(row.findtext("date-created"))
+ room.lastvisited = date_created
+ room.save()
+ except ObjectDoesNotExist:
+ pass # we only care about rooms we know about here
+
+@task(name="meetingtools.apps.room.tasks.mail")
+def send_message(user,subject,message):
+ try:
+ p = user.get_profile()
+ if p and p.email:
+ send_mail(subject,message,settings.NOREPLY,[p.email])
+ else:
+ logging.info("User %s has no email address - email not sent" % user.username)
+ except ObjectDoesNotExist:
+ logging.info("User %s has no profile - email not sent" % user.username)
+ except Exception,exc:
+ logging.error("Error while sending email: \n%s" % exc)
+ send_message.retry(exc=exc)
+
+@periodic_task(run_every=crontab(hour="1", minute="5", day_of_week="*"))
+def clean_old_rooms():
+ for acc in ACCluster.objects.all():
+ then = datetime.now() - timedelta(days=30)
+ then = then.replace(microsecond=0)
+ with ac_api_client(acc) as api:
+ for room in Room.objects.filter(lastvisited__lt=then):
+ logging.debug("room %s was last used %s" % (room.name,humanize.naturalday(room.lastvisited)))
+ send_message.apply_async([room.creator,"You have an unused meetingroom at %s" % acc.name ,"Do you still need %s (%s)?" % (room.name,room.permalink())]) \ No newline at end of file
diff --git a/meetingtools/apps/room/views.py b/meetingtools/apps/room/views.py
new file mode 100644
index 0000000..c46ab6e
--- /dev/null
+++ b/meetingtools/apps/room/views.py
@@ -0,0 +1,548 @@
+'''
+Created on Jan 31, 2011
+
+@author: leifj
+'''
+from meetingtools.apps.room.models import Room, ACCluster
+from meetingtools.multiresponse import respond_to, redirect_to, json_response
+from meetingtools.apps.room.forms import DeleteRoomForm,\
+ CreateRoomForm, ModifyRoomForm, TagRoomForm
+from django.shortcuts import get_object_or_404
+from meetingtools.ac import ac_api_client
+import re
+from meetingtools.apps import room
+from django.contrib.auth.decorators import login_required
+import logging
+from pprint import pformat
+from meetingtools.utils import session, base_url
+import time
+from django.conf import settings
+from django.utils.datetime_safe import datetime
+from django.http import HttpResponseRedirect
+from django.core.exceptions import ObjectDoesNotExist
+from django_co_acls.models import allow, deny, acl, clear_acl
+from meetingtools.ac.api import ACPClient
+from tagging.models import Tag, TaggedItem
+import random, string
+from django.utils.feedgenerator import rfc3339_date
+from django.views.decorators.cache import never_cache
+from meetingtools.apps.cluster.models import acc_for_user
+from django.contrib.auth.models import User
+import iso8601
+from celery.execute import send_task
+from meetingtools.apps.room.tasks import start_user_counts_poll
+
+def _user_meeting_folder(request,acc):
+ if not session(request,'my_meetings_sco_id'):
+ with ac_api_client(acc) as api:
+ userid = request.user.username
+ folders = api.request('sco-search-by-field',{'filter-type': 'folder','field':'name','query':userid}).et.xpath('//sco[folder-name="User Meetings"]')
+ logging.debug("user meetings folder: "+pformat(folders))
+ #folder = next((f for f in folders if f.findtext('.//folder-name') == 'User Meetings'), None)
+ if folders and len(folders) > 0:
+ session(request,'my_meetings_sco_id',folders[0].get('sco-id'))
+
+ return session(request,'my_meetings_sco_id')
+
+def _shared_templates_folder(request,acc):
+ if not session(request,'shared_templates_sco_id'):
+ with ac_api_client(acc) as api:
+ shared = api.request('sco-shortcuts').et.xpath('.//sco[@type="shared-meeting-templates"]')
+ logging.debug("shared templates folder: "+pformat(shared))
+ #folder = next((f for f in folders if f.findtext('.//folder-name') == 'User Meetings'), None)
+ if shared and len(shared) > 0:
+ session(request,'shared_templates_sco_id',shared[0].get('sco-id'))
+ return session(request,'shared_templates_sco_id')
+
+def _user_rooms(request,acc,my_meetings_sco_id):
+ rooms = []
+ if my_meetings_sco_id:
+ with ac_api_client(acc) as api:
+ meetings = api.request('sco-expanded-contents',{'sco-id': my_meetings_sco_id,'filter-type': 'meeting'})
+ if meetings:
+ rooms = [{'sco_id': r.get('sco-id'),
+ 'name': r.findtext('name'),
+ 'source_sco_id': r.get('source-sco-id'),
+ 'urlpath': r.findtext('url-path'),
+ 'description': r.findtext('description')} for r in meetings.et.findall('.//sco')]
+ return rooms
+
+def _user_templates(request,acc,my_meetings_sco_id):
+ templates = []
+ with ac_api_client(acc) as api:
+ if my_meetings_sco_id:
+ my_templates = api.request('sco-contents',{'sco-id': my_meetings_sco_id,'filter-type': 'folder'}).et.xpath('.//sco[folder-name="My Templates"][0]')
+ if my_templates and len(my_templates) > 0:
+ my_templates_sco_id = my_templates[0].get('sco_id')
+ meetings = api.request('sco-contents',{'sco-id': my_templates_sco_id,'filter-type': 'meeting'})
+ if meetings:
+ templates = templates + [(r.get('sco-id'),r.findtext('name')) for r in meetings.et.findall('.//sco')]
+
+ shared_templates_sco_id = _shared_templates_folder(request, acc)
+ if shared_templates_sco_id:
+ shared_templates = api.request('sco-contents',{'sco-id': shared_templates_sco_id,'filter-type': 'meeting'})
+ if shared_templates:
+ templates = templates + [(r.get('sco-id'),r.findtext('name')) for r in shared_templates.et.findall('.//sco')]
+
+ return templates
+
+def _find_current_session(session_info):
+ for r in session_info.et.xpath('//row'):
+ #logging.debug(pformat(etree.tostring(r)))
+ end = r.findtext('date-end')
+ if end is None:
+ return r
+ return None
+
+def _nusers(session_info):
+ cur = _find_current_session(session_info)
+ if cur is not None:
+ return cur.get('num-participants')
+ else:
+ return 0
+
+@login_required
+def view(request,id):
+ room = get_object_or_404(Room,pk=id)
+ return respond_to(request,
+ {'text/html':'apps/room/list.html'},
+ {'user':request.user,
+ 'rooms':[room],
+ 'title': room.name,
+ 'baseurl': base_url(request),
+ 'active': True,
+ })
+
+def _init_update_form(request,form,acc,my_meetings_sco_id):
+ if form.fields.has_key('urlpath'):
+ url = base_url(request)
+ form.fields['urlpath'].widget.prefix = url
+ if form.fields.has_key('source_sco_id'):
+ form.fields['source_sco_id'].widget.choices = [('','-- select template --')]+[r for r in _user_templates(request,acc,my_meetings_sco_id)]
+
+def _update_room(request, room, form=None):
+ params = {'type':'meeting'}
+
+ for attr,param in (('sco_id','sco-id'),('folder_sco_id','folder-id'),('source_sco_id','source-sco-id'),('urlpath','url-path'),('name','name'),('description','description')):
+ v = None
+ if hasattr(room,attr):
+ v = getattr(room,attr)
+ logging.debug("%s,%s = %s" % (attr,param,v))
+ if form and form.cleaned_data.has_key(attr) and form.cleaned_data[attr]:
+ v = form.cleaned_data[attr]
+
+ if v:
+ if isinstance(v,(str,unicode)):
+ params[param] = v
+ elif hasattr(v,'__getitem__'):
+ params[param] = v[0]
+ else:
+ params[param] = repr(v)
+
+ logging.debug(pformat(params))
+ with ac_api_client(room.acc) as api:
+ r = api.request('sco-update', params, True)
+ sco_id = r.et.find(".//sco").get('sco-id')
+ if form:
+ form.cleaned_data['sco_id'] = sco_id
+ form.cleaned_data['source_sco_id'] = r.et.find(".//sco").get('sco-source-id')
+
+ room.sco_id = sco_id
+ room.save()
+
+ user_principal = api.find_user(room.creator.username)
+ #api.request('permissions-reset',{'acl-id': sco_id},True)
+ api.request('permissions-update',{'acl-id': sco_id,'principal-id': user_principal.get('principal-id'),'permission-id':'host'},True) # owner is always host
+
+ if form:
+ if form.cleaned_data.has_key('access'):
+ access = form.cleaned_data['access']
+ if access == 'public':
+ allow(room,'anyone','view-hidden')
+ elif access == 'private':
+ allow(room,'anyone','remove')
+
+ # XXX figure out how to keep the room permissions in sync with the AC permissions
+ for ace in acl(room):
+ principal_id = None
+ if ace.group:
+ principal = api.find_group(ace.group.name)
+ if principal:
+ principal_id = principal.get('principal-id')
+ elif ace.user:
+ principal = api.find_user(ace.user.username)
+ if principal:
+ principal_id = principal.get('principal-id')
+ else:
+ principal_id = "public-access"
+
+ if principal_id:
+ api.request('permissions-update',{'acl-id': room.sco_id, 'principal-id': principal_id, 'permission-id': ace.permission},True)
+
+ room.deleted_sco_id = None # if we just cleaned a room we zero out the deleted_sco_id field to indicate the room is now ready for use
+ room.save() # a second save here to avoid races
+ return room
+
+@never_cache
+@login_required
+def create(request):
+ acc = acc_for_user(request.user)
+ my_meetings_sco_id = _user_meeting_folder(request,acc)
+ template_sco_id = acc.default_template_sco_id
+ if not template_sco_id:
+ template_sco_id = DEFAULT_TEMPLATE_SCO
+ room = Room(creator=request.user,acc=acc,folder_sco_id=my_meetings_sco_id,source_sco_id=template_sco_id)
+ what = "Create"
+ title = "Create a new room"
+
+ if request.method == 'POST':
+ form = CreateRoomForm(request.POST,instance=room)
+ _init_update_form(request, form, acc, room.folder_sco_id)
+ if form.is_valid():
+ _update_room(request, room, form)
+ room = form.save()
+ return redirect_to("/rooms#%d" % room.id)
+ else:
+ form = CreateRoomForm(instance=room)
+ _init_update_form(request, form, acc, room.folder_sco_id)
+
+ return respond_to(request,{'text/html':'apps/room/create.html'},{'form':form,'formtitle': title,'cancelname':'Cancel','submitname':'%s Room' % what})
+
+@never_cache
+@login_required
+def update(request,id):
+ room = get_object_or_404(Room,pk=id)
+ acc = room.acc
+ what = "Update"
+ title = "Modify %s" % room.name
+
+ if request.method == 'POST':
+ form = ModifyRoomForm(request.POST,instance=room)
+ _init_update_form(request, form, acc, room.folder_sco_id)
+ if form.is_valid():
+ _update_room(request, room, form)
+ room = form.save()
+ return redirect_to("/rooms#%d" % room.id)
+ else:
+ form = ModifyRoomForm(instance=room)
+ _init_update_form(request, form, acc, room.folder_sco_id)
+
+ return respond_to(request,{'text/html':'apps/room/update.html'},{'form':form,'formtitle': title,'cancelname': 'Cancel','submitname':'%s Room' % what})
+
+def _import_room(request,acc,r):
+ modified = False
+ room = None
+
+ if room and (abs(room.lastupdate() - time.time()) < settings.IMPORT_TTL):
+ return room
+
+ if r.has_key('urlpath'):
+ r['urlpath'] = r['urlpath'].strip('/')
+
+ try:
+ room = Room.objects.get(sco_id=r['sco_id'],acc=acc)
+ for key in ('sco_id','name','source_sco_id','urlpath','description','user_count','host_count'):
+ if r.has_key(key) and hasattr(room,key):
+ rv = getattr(room,key)
+ if rv != r[key] and r[key] != None and r[key]:
+ setattr(room,key,r[key])
+ modified = True
+
+ if modified:
+ logging.debug("+++ saving room ... %s" % pformat(room))
+ room.save()
+
+ except ObjectDoesNotExist:
+ if r['folder_sco_id']:
+ try:
+ room = Room.objects.create(sco_id=r['sco_id'],
+ source_sco_id=r['source_sco_id'],
+ acc=acc,
+ name=r['name'],
+ urlpath=r['urlpath'],
+ description=r['description'],
+ creator=request.user,
+ folder_sco_id=r['folder_sco_id'])
+ except Exception,e:
+ room = None
+ pass
+
+ if not room:
+ return None
+
+ logging.debug("+++ looking at user counts")
+ with ac_api_client(acc) as api:
+ userlist = api.request('meeting-usermanager-user-list',{'sco-id': room.sco_id},False)
+ if userlist.status_code() == 'ok':
+ room.user_count = int(userlist.et.xpath("count(.//userdetails)"))
+ room.host_count = int(userlist.et.xpath("count(.//userdetails/role[text() = 'host'])"))
+ room.save()
+
+ return room
+
+@login_required
+def list_rooms(request,username=None):
+ user = request.user
+ if username:
+ try:
+ user = User.objects.get(username=username)
+ except ObjectDoesNotExist:
+ user = None
+
+ rooms = []
+ if user:
+ rooms = Room.objects.filter(creator=user).order_by('name').all()
+
+ return respond_to(request,
+ {'text/html':'apps/room/list.html'},
+ {'title':'Your Rooms','edit':True,'active':len(rooms) == 1,'rooms':rooms})
+
+@login_required
+def user_rooms(request,user=None):
+ if user is None:
+ user = request.user
+
+ acc = acc_for_user(user)
+ my_meetings_sco_id = _user_meeting_folder(request,acc)
+ user_rooms = _user_rooms(request,acc,my_meetings_sco_id)
+
+ ar = []
+ for r in user_rooms:
+ logging.debug(pformat(r))
+ ar.append(int(r['sco_id']))
+
+ for r in Room.objects.filter(creator=user).all():
+ if (not r.sco_id in ar): # and (not r.self_cleaning): #XXX this logic isn't right!
+ for t in Tag.objects.get_for_object(r):
+ t.delete()
+ r.delete()
+
+ for r in user_rooms:
+ r['folder_sco_id'] = my_meetings_sco_id
+ room = _import_room(request,acc,r)
+
+ rooms = Room.objects.filter(creator=user).order_by('name').all()
+ return respond_to(request,
+ {'text/html':'apps/room/list.html'},
+ {'title':'Your Rooms','edit':True,'active':len(rooms) == 1,'rooms':rooms})
+
+@login_required
+def unlock(request,id):
+ room = get_object_or_404(Room,pk=id)
+ room.unlock()
+ return redirect_to("/rooms#%d" % room.id)
+
+@login_required
+def delete(request,id):
+ room = get_object_or_404(Room,pk=id)
+ if request.method == 'POST':
+ form = DeleteRoomForm(request.POST)
+ if form.is_valid():
+ with ac_api_client(room.acc) as api:
+ api.request('sco-delete',{'sco-id':room.sco_id},raise_error=True)
+ clear_acl(room)
+ room.delete()
+
+ return redirect_to("/rooms")
+ else:
+ form = DeleteRoomForm()
+
+ return respond_to(request,{'text/html':'edit.html'},{'form':form,'formtitle': 'Delete %s' % room.name,'cancelname':'Cancel','submitname':'Delete Room'})
+
+def _clean(request,room):
+ with ac_api_client(room.acc) as api:
+ room.deleted_sco_id = room.sco_id
+ room.save()
+ api.request('sco-delete',{'sco-id':room.sco_id},raise_error=False)
+ room.sco_id = None
+ return _update_room(request, room)
+
+def occupation(request,rid):
+ room = get_object_or_404(Room,pk=rid)
+ with ac_api_client(room.acc) as api:
+ api.poll_user_counts(room)
+ d = {'nusers': room.user_count, 'nhosts': room.host_count}
+ return respond_to(request,
+ {'text/html': 'apps/room/fragments/occupation.txt',
+ 'application/json': json_response(d, request)},
+ d)
+
+def go_by_id(request,id):
+ room = get_object_or_404(Room,pk=id)
+ return goto(request,room)
+
+def go_by_path(request,path):
+ room = get_object_or_404(Room,urlpath=path)
+ return goto(request,room)
+
+@login_required
+def promote_and_launch(request,rid):
+ room = get_object_or_404(Room,pk=rid)
+ return _goto(request,room,clean=False,promote=True)
+
+def launch(request,rid):
+ room = get_object_or_404(Room,pk=rid)
+ return _goto(request,room,clean=False)
+
+def goto(request,room):
+ return _goto(request,room,clean=True)
+
+def _random_key(length=20):
+ rg = random.SystemRandom()
+ alphabet = string.letters + string.digits
+ return str().join(rg.choice(alphabet) for _ in range(length))
+
+def _goto(request,room,clean=True,promote=False):
+ if room.is_locked():
+ return respond_to(request, {"text/html": "apps/room/retry.html"}, {'room': room, 'wait': 10})
+
+ now = time.time()
+ lastvisit = room.lastvisit()
+ room.lastvisited = datetime.now()
+
+ with ac_api_client(room.acc) as api:
+ api.poll_user_counts(room)
+ if clean:
+ # don't clean the room unless you get a good status code from the call to the usermanager api above
+ if room.self_cleaning and room.user_count == 0:
+ if (room.user_count == 0) and (abs(lastvisit - now) > settings.GRACE):
+ room.lock("Locked for cleaning")
+ try:
+ room = _clean(request,room)
+ except Exception,e:
+ room.unlock()
+ raise e
+ room.unlock()
+
+ if room.host_count == 0 and room.allow_host:
+ return respond_to(request, {"text/html": "apps/room/launch.html"}, {'room': room})
+ else:
+ room.save()
+
+ key = None
+ if request.user.is_authenticated():
+ key = _random_key(20)
+ user_principal = api.find_user(request.user.username)
+ principal_id = user_principal.get('principal-id')
+ with ac_api_client(room.acc) as api:
+ api.request("user-update-pwd",{"user-id": principal_id, 'password': key,'password-verify': key},True)
+ if promote and room.self_cleaning:
+ if user_principal:
+ api.request('permissions-update',{'acl-id': room.sco_id,'principal-id': user_principal.get('principal-id'),'permission-id':'host'},True)
+
+ r = api.request('sco-info',{'sco-id':room.sco_id},True)
+ urlpath = r.et.findtext('.//sco/url-path')
+ start_user_counts_poll(room,10)
+ if key:
+ try:
+ user_client = ACPClient(room.acc.api_url, request.user.username, key, cache=False)
+ return user_client.redirect_to(room.acc.url+urlpath)
+ except Exception,e:
+ pass
+ return HttpResponseRedirect(room.acc.url+urlpath)
+
+## Tagging
+
+def _room2dict(room):
+ return {'name':room.name,
+ 'description':room.description,
+ 'user_count':room.nusers(),
+ 'host_count':room.nhosts(),
+ 'updated': rfc3339_date(room.lastupdated),
+ 'self_cleaning': room.self_cleaning,
+ 'url': room.go_url()}
+
+# should not require login
+def list_by_tag(request,tn):
+ tags = tn.split('+')
+ rooms = TaggedItem.objects.get_by_model(Room, tags).order_by('name').all()
+ title = 'Rooms tagged with %s' % " and ".join(tags)
+ return respond_to(request,
+ {'text/html':'apps/room/list.html',
+ 'application/json': json_response([_room2dict(room) for room in rooms],request)},
+ {'title':title,
+ 'description':title ,
+ 'edit':False,
+ 'active':len(rooms) == 1,
+ 'baseurl': base_url(request),
+ 'tagstring': tn,
+ 'rooms':rooms})
+
+# should not require login
+def list_and_import_by_tag(request,tn):
+ tags = tn.split('+')
+ rooms = TaggedItem.objects.get_by_model(Room, tags).order_by('name').all()
+ for room in rooms:
+ _import_room(request,room.acc,{'sco_id': room.sco_id})
+ title = 'Rooms tagged with %s' % " and ".join(tags)
+ return respond_to(request,
+ {'text/html':'apps/room/list.html',
+ 'application/json': json_response([_room2dict(room) for room in rooms],request)},
+ {'title':title,
+ 'description':title ,
+ 'edit':False,
+ 'active':len(rooms) == 1,
+ 'baseurl': base_url(request),
+ 'tagstring': tn,
+ 'rooms':rooms})
+
+def widget(request,tags=None):
+ title = 'Meetingtools jQuery widget'
+ return respond_to(request,{'text/html':'apps/room/widget.html'},{'title': title,'tags':tags})
+
+def _can_tag(request,tag):
+ if tag in ('selfcleaning','cleaning','public','private'):
+ return False,"'%s' is reserved" % tag
+ # XXX implement access model for tags here soon
+ return True,""
+
+@login_required
+def untag(request,rid,tag):
+ room = get_object_or_404(Room,pk=rid)
+ new_tags = []
+ for t in Tag.objects.get_for_object(room):
+ if t.name != tag:
+ new_tags.append(t.name)
+
+ Tag.objects.update_tags(room, ' '.join(new_tags))
+ return redirect_to("/room/%d/tag" % room.id)
+
+@never_cache
+@login_required
+def tag(request,rid):
+ room = get_object_or_404(Room,pk=rid)
+ if request.method == 'POST':
+ form = TagRoomForm(request.POST)
+ if form.is_valid():
+ for tag in re.split('[,\s]+',form.cleaned_data['tag']):
+ tag = tag.strip()
+ ok,reason = _can_tag(request,tag)
+ if ok:
+ Tag.objects.add_tag(room, tag)
+ else:
+ form._errors['tag'] = form.error_class([u'%s ... please choose another tag!' % reason])
+ else:
+ form = TagRoomForm()
+
+ tags = Tag.objects.get_for_object(room)
+ tn = "+".join([t.name for t in tags])
+ return respond_to(request,
+ {'text/html': "apps/room/tag.html"},
+ {'form': form,'formtitle': 'Add Tag','cancelname':'Done','submitname': 'Add Tag','room': room, 'tagstring': tn,'tags': tags})
+
+def room_recordings(request,room):
+ with ac_api_client(room.acc) as api:
+ r = api.request('sco-expanded-contents',{'sco-id': room.sco_id,'filter-icon':'archive'},True)
+ return [{'name': sco.findtext('name'),
+ 'sco_id': sco.get('sco-id'),
+ 'url': room.acc.make_url(sco.findtext('url-path')),
+ 'description': sco.findtext('description'),
+ 'date_created': iso8601.parse_date(sco.findtext('date-created')),
+ 'date_modified': iso8601.parse_date(sco.findtext('date-modified'))} for sco in r.et.findall(".//sco")]
+
+@login_required
+def recordings(request,rid):
+ room = get_object_or_404(Room,pk=rid)
+ return respond_to(request,
+ {'text/html': 'apps/room/recordings.html'},
+ {'recordings': room_recordings(request,room),'room':room}) \ No newline at end of file
diff --git a/meetingtools/apps/stats/__init__.py b/meetingtools/apps/stats/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/meetingtools/apps/stats/__init__.py
diff --git a/meetingtools/apps/stats/forms.py b/meetingtools/apps/stats/forms.py
new file mode 100644
index 0000000..d9cf555
--- /dev/null
+++ b/meetingtools/apps/stats/forms.py
@@ -0,0 +1,11 @@
+'''
+Created on Jan 16, 2012
+
+@author: leifj
+'''
+from django.forms.forms import Form
+from django.forms.fields import DateTimeField
+
+class StatCaledarForm(Form):
+ begin = DateTimeField(required=False)
+ end = DateTimeField(required=False) \ No newline at end of file
diff --git a/meetingtools/apps/stats/views.py b/meetingtools/apps/stats/views.py
new file mode 100644
index 0000000..b028d18
--- /dev/null
+++ b/meetingtools/apps/stats/views.py
@@ -0,0 +1,273 @@
+'''
+Created on Jan 16, 2012
+
+@author: leifj
+'''
+from django.contrib.auth.decorators import login_required
+from django.http import HttpResponseForbidden, HttpResponseBadRequest
+from meetingtools.ac import ac_api_client
+from iso8601 import iso8601
+from time import mktime
+from meetingtools.multiresponse import json_response, respond_to
+from meetingtools.apps.stats.forms import StatCaledarForm
+from django.shortcuts import get_object_or_404
+from meetingtools.apps.room.models import Room
+import logging
+
+def _iso2datesimple(iso):
+ (date,rest) = iso.split("T")
+ return date
+
+def _iso2ts(iso):
+ return mktime(iso8601.parse_date(iso).timetuple())*1000
+
+def _iso2dt(iso):
+ return iso8601.parse_date(iso);
+
+def _date_ts(date):
+ (y,m,d) = date.split("-")
+ return int(mktime((int(y),int(m),int(d),0,0,0,0,0,-1)))*1000 # midnight
+
+@login_required
+def user(request,username=None):
+ if username == None:
+ username = request.user.username
+ (local,domain) = username.split('@')
+ return respond_to(request,{'text/html': 'apps/stats/user.html'},{'domain': domain,'username': username})
+
+@login_required
+def domain(request,domain):
+ (l,d) = request.user.username.split('@')
+ if d != domain:
+ return HttpResponseForbidden("You can only look at statistics for your own domain!")
+
+ return respond_to(request,{'text/html': 'apps/stats/domain.html'},{'domain': domain})
+
+@login_required
+def room(request,rid):
+ room = get_object_or_404(Room,pk=rid)
+ if not room.creator == request.user:
+ return HttpResponseForbidden("You can only look at statistics for your own rooms!")
+
+ return respond_to(request,{'text/html': 'apps/stats/room.html'},{'room': room})
+
+@login_required
+def user_minutes_api(request,username=None):
+ #if username and username != request.user.username:
+ # return HttpResponseForbidden("You can't spy on others!")
+
+ if username == None:
+ username = request.user.username
+
+ with ac_api_client(request) as api:
+ p = {'sort1-type': 'asc','sort2-type': 'asc','sort1': 'date-created','sort2': 'date-closed','filter-type': 'meeting','filter-login':username}
+
+ form = StatCaledarForm(request.GET)
+ if not form.is_valid():
+ return HttpResponseBadRequest()
+
+ begin = form.cleaned_data['begin']
+ end = form.cleaned_data['end']
+
+ if begin != None:
+ p['filter-gte-date-created'] = begin
+ if end != None:
+ p['filter-lt-date-created'] = end
+ r = api.request('report-bulk-consolidated-transactions',p)
+
+ series = []
+ d_created = None
+ d_closed = None
+ ms = 0
+ curdate = None
+ t_ms = 0
+ rc = {}
+ for row in r.et.xpath("//row"):
+ rc[row.get('sco-id')] = True
+ date_created_str = row.findtext("date-created")
+ ts_created = _iso2ts(date_created_str)
+ date_closed_str = row.findtext("date-closed")
+ ts_closed = _iso2ts(date_closed_str)
+
+ d1 = _iso2datesimple(date_created_str)
+ if d_created == None:
+ d_created = d1
+
+ d2 = _iso2datesimple(date_closed_str)
+ if d_closed == None:
+ d_closed = d2
+
+ #duration = _iso2dt(date_closed_str) - _iso2dt(date_created_str)
+ #sdiff = duration.total_seconds()
+
+ if curdate == None:
+ curdate = d1
+
+ if curdate != d1:
+ #logging.debug(" %s: %s - %s = %d %d" % (row.findtext("name"),date_created_str,date_closed_str,ms,sdiff*1000))
+ series.append([_date_ts(curdate),int(ms/60000)])
+ ms = 0
+ curdate = d1
+
+ if d1 == d2: #same date
+ diff = (ts_closed - ts_created)
+ #logging.debug("ms:: %d + %d" % (ms,diff))
+ ms = ms + diff
+ t_ms = t_ms + diff
+ else: # meeting spanned midnight
+ ts_date_ts = _date_ts(d2)
+ #logging.debug("ms: %d + %d" % (ms,(ts_date_ts - ts_created)))
+ ms = ms + (ts_date_ts - ts_created)
+ series.append([_date_ts(d1),int(ms/60000)])
+ #logging.debug("* %s: %s - %s = %d %d" % (row.findtext("name"),date_created_str,date_closed_str,ms,sdiff*1000))
+ t_ms = t_ms + ms
+ curdate = d2
+ #logging.debug("midnight: %d (%d)" % (ts_date_ts,ts_closed))
+ ms = (ts_closed - ts_date_ts)
+ #logging.debug("nms: %d" % ms)
+
+ if curdate != None and ms > 0:
+ series.append([_date_ts(curdate),int(ms/60000)])
+
+ return json_response({'data': sorted(series,key=lambda x: x[0]), 'rooms': len(rc.keys()), 'minutes': int(t_ms/60000)},request)
+
+@login_required
+def domain_minutes_api(request,domain):
+ with ac_api_client(request) as api:
+ p = {'sort': 'asc','sort1': 'date-created','filter-type': 'meeting'}
+
+ form = StatCaledarForm(request.GET)
+ if not form.is_valid():
+ return HttpResponseBadRequest()
+
+ begin = form.cleaned_data['begin']
+ end = form.cleaned_data['end']
+
+ if begin != None:
+ p['filter-gte-date-created'] = begin
+ if end != None:
+ p['filter-lt-date-created'] = end
+ r = api.request('report-bulk-consolidated-transactions',p)
+
+ series = []
+ d_created = None
+ d_closed = None
+ ms = 0
+ curdate = None
+ t_ms = 0
+ rc = {}
+ uc = {}
+ for row in r.et.xpath("//row"):
+ login = row.findtext("login")
+ if not login.endswith("@%s" % domain):
+ continue
+
+ rc[row.get('sco-id')] = True
+ uc[row.get('principal-id')] = True
+ date_created_str = row.findtext("date-created")
+ ts_created = _iso2ts(date_created_str)
+ date_closed_str = row.findtext("date-closed")
+ ts_closed = _iso2ts(date_closed_str)
+
+ d1 = _iso2datesimple(date_created_str)
+ if d_created == None:
+ d_created = d1
+
+ d2 = _iso2datesimple(date_closed_str)
+ if d_closed == None:
+ d_closed = d2
+
+ if curdate == None:
+ curdate = d1
+
+ if curdate != d1:
+ series.append([_date_ts(curdate),int(ms/60000)])
+ ms = 0
+ curdate = d1
+
+ if d1 == d2: #same date
+ diff = (ts_closed - ts_created)
+ ms = ms + diff
+ t_ms = t_ms + diff
+ else: # meeting spanned midnight
+ ts_date_ts = _date_ts(d2)
+ ms = ms + (ts_date_ts - ts_created)
+ series.append([_date_ts(d1),int(ms/60000)])
+ t_ms = t_ms + ms
+ curdate = d2
+ ms = (ts_closed - ts_date_ts)
+
+ if curdate != None and ms > 0:
+ series.append([_date_ts(curdate),int(ms/60000)])
+
+ return json_response({'data': sorted(series,key=lambda x: x[0]), 'rooms': len(rc.keys()), 'users': len(uc.keys()), 'minutes': int(t_ms/60000)},request)
+
+
+@login_required
+def room_minutes_api(request,rid):
+ room = get_object_or_404(Room,pk=rid)
+ if not room.creator == request.user:
+ return HttpResponseForbidden("You can only look at statistics for your own rooms!")
+
+ with ac_api_client(request) as api:
+ p = {'sort': 'asc','sort1': 'date-created','filter-type': 'meeting','filter-sco-id': room.sco_id}
+
+ form = StatCaledarForm(request.GET)
+ if not form.is_valid():
+ return HttpResponseBadRequest()
+
+ begin = form.cleaned_data['begin']
+ end = form.cleaned_data['end']
+
+ if begin != None:
+ p['filter-gte-date-created'] = begin
+ if end != None:
+ p['filter-lt-date-created'] = end
+ r = api.request('report-bulk-consolidated-transactions',p)
+
+ series = []
+ d_created = None
+ d_closed = None
+ ms = 0
+ curdate = None
+ t_ms = 0
+ uc = {}
+ for row in r.et.xpath("//row"):
+ uc[row.get('principal-id')] = True
+ date_created_str = row.findtext("date-created")
+ ts_created = _iso2ts(date_created_str)
+ date_closed_str = row.findtext("date-closed")
+ ts_closed = _iso2ts(date_closed_str)
+
+ d1 = _iso2datesimple(date_created_str)
+ if d_created == None:
+ d_created = d1
+
+ d2 = _iso2datesimple(date_closed_str)
+ if d_closed == None:
+ d_closed = d2
+
+ if curdate == None:
+ curdate = d1
+
+ if curdate != d1:
+ series.append([_date_ts(curdate),int(ms/60000)])
+ ms = 0
+ curdate = d1
+
+ if d1 == d2: #same date
+ diff = (ts_closed - ts_created)
+ ms = ms + diff
+ t_ms = t_ms + diff
+ else: # meeting spanned midnight
+ ts_date_ts = _date_ts(d2)
+ ms = ms + (ts_date_ts - ts_created)
+ series.append([_date_ts(d1),int(ms/60000)])
+ t_ms = t_ms + ms
+ curdate = d2
+ ms = (ts_closed - ts_date_ts)
+
+ if curdate != None and ms > 0:
+ series.append([_date_ts(curdate),int(ms/60000)])
+
+ return json_response({'data': sorted(series,key=lambda x: x[0]), 'users': len(uc.keys()), 'minutes': int(t_ms/60000)},request) \ No newline at end of file
diff --git a/meetingtools/apps/userprofile/__init__.py b/meetingtools/apps/userprofile/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/meetingtools/apps/userprofile/__init__.py
diff --git a/meetingtools/apps/userprofile/admin.py b/meetingtools/apps/userprofile/admin.py
new file mode 100644
index 0000000..21ca598
--- /dev/null
+++ b/meetingtools/apps/userprofile/admin.py
@@ -0,0 +1,4 @@
+from django.contrib import admin
+from meetingtools.apps.userprofile.models import UserProfile
+
+admin.site.register(UserProfile) \ No newline at end of file
diff --git a/meetingtools/apps/userprofile/models.py b/meetingtools/apps/userprofile/models.py
new file mode 100644
index 0000000..b0bc7ae
--- /dev/null
+++ b/meetingtools/apps/userprofile/models.py
@@ -0,0 +1,21 @@
+'''
+Created on Jul 5, 2010
+
+@author: leifj
+'''
+from django.db import models
+from django.contrib.auth.models import User
+
+class UserProfile(models.Model):
+ user = models.ForeignKey(User,blank=True,related_name='profile')
+ display_name = models.CharField(max_length=255,blank=True)
+ email = models.EmailField(blank=True)
+ idp = models.CharField(max_length=255)
+ timecreated = models.DateTimeField(auto_now_add=True)
+ lastupdated = models.DateTimeField(auto_now=True)
+
+ def __unicode__(self):
+ return "%s - %s" % (self.user.username,self.display_name)
+
+def profile(user):
+ return UserProfile.objects.get(user=user)
diff --git a/meetingtools/context_processors.py b/meetingtools/context_processors.py
new file mode 100644
index 0000000..5bd5885
--- /dev/null
+++ b/meetingtools/context_processors.py
@@ -0,0 +1,23 @@
+from django.core.exceptions import ImproperlyConfigured
+
+__author__ = 'leifj'
+
+from django.conf import settings
+import logging
+
+def theme(request):
+
+ def _w(x):
+ return {'theme': x}
+
+ vhost = request.get_host()
+ vhost = vhost.replace(':','_')
+
+ ctx = {'vhost': vhost}
+ if hasattr(settings,'THEMES'):
+ if settings.THEMES.has_key(vhost):
+ ctx.update(settings.THEMES[vhost])
+ elif settings.THEMES.has_key('__default__'):
+ ctx.update(settings.THEMES['__default__'])
+
+ return _w(ctx)
diff --git a/meetingtools/django-crossdomainxhr-middleware.py b/meetingtools/django-crossdomainxhr-middleware.py
new file mode 100644
index 0000000..786b72a
--- /dev/null
+++ b/meetingtools/django-crossdomainxhr-middleware.py
@@ -0,0 +1,44 @@
+import re
+
+from django.utils.text import compress_string
+from django.utils.cache import patch_vary_headers
+
+from django import http
+
+try:
+ import settings
+ XS_SHARING_ALLOWED_ORIGINS = settings.XS_SHARING_ALLOWED_ORIGINS
+ XS_SHARING_ALLOWED_METHODS = settings.XS_SHARING_ALLOWED_METHODS
+except:
+ XS_SHARING_ALLOWED_ORIGINS = '*'
+ XS_SHARING_ALLOWED_METHODS = ['POST','GET','OPTIONS', 'PUT', 'DELETE']
+
+
+class XsSharing(object):
+ """
+ This middleware allows cross-domain XHR using the html5 postMessage API.
+
+
+ Access-Control-Allow-Origin: http://foo.example
+ Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE
+ """
+ def process_request(self, request):
+
+ if 'HTTP_ACCESS_CONTROL_REQUEST_METHOD' in request.META:
+ response = http.HttpResponse()
+ response['Access-Control-Allow-Origin'] = XS_SHARING_ALLOWED_ORIGINS
+ response['Access-Control-Allow-Methods'] = ",".join( XS_SHARING_ALLOWED_METHODS )
+ response['P3P'] = "CP=IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"
+ return response
+
+ return None
+
+ def process_response(self, request, response):
+ # Avoid unnecessary work
+ if response.has_header('Access-Control-Allow-Origin'):
+ return response
+
+ response['Access-Control-Allow-Origin'] = XS_SHARING_ALLOWED_ORIGINS
+ response['Access-Control-Allow-Methods'] = ",".join( XS_SHARING_ALLOWED_METHODS )
+ response['P3P'] = "CP=IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"
+ return response \ No newline at end of file
diff --git a/meetingtools/extensions/__init__.py b/meetingtools/extensions/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/meetingtools/extensions/__init__.py
diff --git a/meetingtools/extensions/templatetags/__init__.py b/meetingtools/extensions/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/meetingtools/extensions/templatetags/__init__.py
diff --git a/meetingtools/extensions/templatetags/datehumanize.py b/meetingtools/extensions/templatetags/datehumanize.py
new file mode 100644
index 0000000..9b75cae
--- /dev/null
+++ b/meetingtools/extensions/templatetags/datehumanize.py
@@ -0,0 +1,36 @@
+from django import template
+from django.template import defaultfilters
+
+register = template.Library()
+
+MOMENT = 120 # duration in seconds within which the time difference
+ # will be rendered as 'a moment ago'
+
+def datehumanize(value):
+ """
+ Finds the difference between the datetime value given and now()
+ and returns appropriate humanize form
+ """
+
+ from datetime import datetime
+
+ if isinstance(value, datetime):
+ value = value.replace(tzinfo=None)
+ delta = datetime.now() - value
+ if delta.days > 6:
+ return value.strftime("on %b %d") # May 15
+ if delta.days > 1:
+ return value.strftime("on %A") # Wednesday
+ elif delta.days == 1:
+ return 'yesterday' # yesterday
+ elif delta.seconds > 3600:
+ return str(delta.seconds / 3600 ) + ' hours ago' # 3 hours ago
+ elif delta.seconds > MOMENT:
+ return str(delta.seconds/60) + ' minutes ago' # 29 minutes ago
+ else:
+ return 'a moment ago' # a moment ago
+ return defaultfilters.date(value)
+ else:
+ return str(value)
+datehumanize.is_safe = True
+register.filter(datehumanize) \ No newline at end of file
diff --git a/meetingtools/extensions/templatetags/roomurl.py b/meetingtools/extensions/templatetags/roomurl.py
new file mode 100644
index 0000000..e5abe57
--- /dev/null
+++ b/meetingtools/extensions/templatetags/roomurl.py
@@ -0,0 +1,19 @@
+from django import template
+
+register = template.Library()
+
+MOMENT = 120 # duration in seconds within which the time difference
+ # will be rendered as 'a moment ago'
+
+def roomurl(room):
+ """
+ Display the public 'go' URL of a meetingroom
+ """
+ path = room.id
+ if room.urlpath:
+ path = room.urlpath
+
+ return "/go/%s" % path
+
+roomurl.is_safe = True
+register.filter(roomurl) \ No newline at end of file
diff --git a/meetingtools/manage.py b/meetingtools/manage.py
new file mode 100644
index 0000000..5e78ea9
--- /dev/null
+++ b/meetingtools/manage.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+from django.core.management import execute_manager
+try:
+ import settings # Assumed to be in the same directory.
+except ImportError:
+ import sys
+ sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+ sys.exit(1)
+
+if __name__ == "__main__":
+ execute_manager(settings)
diff --git a/meetingtools/mimeparse.py b/meetingtools/mimeparse.py
new file mode 100644
index 0000000..0fd91e7
--- /dev/null
+++ b/meetingtools/mimeparse.py
@@ -0,0 +1,123 @@
+"""MIME-Type Parser
+
+This module provides basic functions for handling mime-types. It can handle
+matching mime-types against a list of media-ranges. See section 14.1 of
+the HTTP specification [RFC 2616] for a complete explanation.
+
+ http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
+
+Contents:
+ - parse_mime_type(): Parses a mime-type into its component parts.
+ - parse_media_range(): Media-ranges are mime-types with wild-cards and a 'q' quality parameter.
+ - quality(): Determines the quality ('q') of a mime-type when compared against a list of media-ranges.
+ - quality_parsed(): Just like quality() except the second parameter must be pre-parsed.
+ - best_match(): Choose the mime-type with the highest quality ('q') from a list of candidates.
+"""
+
+__version__ = "0.1.2"
+__author__ = 'Joe Gregorio'
+__email__ = "joe@bitworking.org"
+__credits__ = ""
+
+def parse_mime_type(mime_type):
+ """Carves up a mime-type and returns a tuple of the
+ (type, subtype, params) where 'params' is a dictionary
+ of all the parameters for the media range.
+ For example, the media range 'application/xhtml;q=0.5' would
+ get parsed into:
+
+ ('application', 'xhtml', {'q', '0.5'})
+ """
+ parts = mime_type.split(";")
+ params = dict([tuple([s.strip() for s in param.split("=")])\
+ for param in parts[1:] ])
+ full_type = parts[0].strip()
+ # Java URLConnection class sends an Accept header that includes a single "*"
+ # Turn it into a legal wildcard.
+ if full_type == '*': full_type = '*/*'
+ (type, subtype) = full_type.split("/")
+ return (type.strip(), subtype.strip(), params)
+
+def parse_media_range(range):
+ """Carves up a media range and returns a tuple of the
+ (type, subtype, params) where 'params' is a dictionary
+ of all the parameters for the media range.
+ For example, the media range 'application/*;q=0.5' would
+ get parsed into:
+
+ ('application', '*', {'q', '0.5'})
+
+ In addition this function also guarantees that there
+ is a value for 'q' in the params dictionary, filling it
+ in with a proper default if necessary.
+ """
+ (type, subtype, params) = parse_mime_type(range)
+ if not params.has_key('q') or not params['q'] or \
+ not float(params['q']) or float(params['q']) > 1\
+ or float(params['q']) < 0:
+ params['q'] = '1'
+ return (type, subtype, params)
+
+def fitness_and_quality_parsed(mime_type, parsed_ranges):
+ """Find the best match for a given mime-type against
+ a list of media_ranges that have already been
+ parsed by parse_media_range(). Returns a tuple of
+ the fitness value and the value of the 'q' quality
+ parameter of the best match, or (-1, 0) if no match
+ was found. Just as for quality_parsed(), 'parsed_ranges'
+ must be a list of parsed media ranges. """
+ best_fitness = -1
+ best_fit_q = 0
+ (target_type, target_subtype, target_params) =\
+ parse_media_range(mime_type)
+ for (type, subtype, params) in parsed_ranges:
+ if (type == target_type or type == '*' or target_type == '*') and \
+ (subtype == target_subtype or subtype == '*' or target_subtype == '*'):
+ param_matches = reduce(lambda x, y: x+y, [1 for (key, value) in \
+ target_params.iteritems() if key != 'q' and \
+ params.has_key(key) and value == params[key]], 0)
+ fitness = (type == target_type) and 100 or 0
+ fitness += (subtype == target_subtype) and 10 or 0
+ fitness += param_matches
+ if fitness > best_fitness:
+ best_fitness = fitness
+ best_fit_q = params['q']
+
+ return best_fitness, float(best_fit_q)
+
+def quality_parsed(mime_type, parsed_ranges):
+ """Find the best match for a given mime-type against
+ a list of media_ranges that have already been
+ parsed by parse_media_range(). Returns the
+ 'q' quality parameter of the best match, 0 if no
+ match was found. This function bahaves the same as quality()
+ except that 'parsed_ranges' must be a list of
+ parsed media ranges. """
+ return fitness_and_quality_parsed(mime_type, parsed_ranges)[1]
+
+def quality(mime_type, ranges):
+ """Returns the quality 'q' of a mime-type when compared
+ against the media-ranges in ranges. For example:
+
+ >>> quality('text/html','text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5')
+ 0.7
+
+ """
+ parsed_ranges = [parse_media_range(r) for r in ranges.split(",")]
+ return quality_parsed(mime_type, parsed_ranges)
+
+def best_match(supported, header):
+ """Takes a list of supported mime-types and finds the best
+ match for all the media-ranges listed in header. The value of
+ header must be a string that conforms to the format of the
+ HTTP Accept: header. The value of 'supported' is a list of
+ mime-types.
+
+ >>> best_match(['application/xbel+xml', 'text/xml'], 'text/*;q=0.5,*/*; q=0.1')
+ 'text/xml'
+ """
+ parsed_header = [parse_media_range(r) for r in header.split(",")]
+ weighted_matches = [(fitness_and_quality_parsed(mime_type, parsed_header), mime_type)\
+ for mime_type in supported]
+ weighted_matches.sort()
+ return weighted_matches[-1][0][1] and weighted_matches[-1][1] or ''
diff --git a/meetingtools/multiresponse.py b/meetingtools/multiresponse.py
new file mode 100644
index 0000000..f599323
--- /dev/null
+++ b/meetingtools/multiresponse.py
@@ -0,0 +1,77 @@
+from meetingtools import context_processors
+import meetingtools.mimeparse as mimeparse
+import re
+import rfc822
+from django.conf import settings
+from django.shortcuts import render_to_response
+from django.http import HttpResponse, HttpResponseForbidden,\
+ HttpResponseRedirect
+from django.utils import simplejson
+from django.template import loader, RequestContext
+
+default_suffix_mapping = {"\.htm(l?)$": "text/html",
+ "\.json$": "application/json",
+ "\.rss$": "application/rss+xml",
+ "\.atom$": "application/atom+xml",
+ "\.torrent$": "application/x-bittorrent"}
+
+def _accept_types(request, suffix):
+ for r in suffix.keys():
+ p = re.compile(r)
+ if p.search(request.path):
+ return suffix.get(r)
+ return None
+
+
+def timeAsrfc822 ( theTime ) :
+ return rfc822 . formatdate ( rfc822 . mktime_tz ( rfc822 . parsedate_tz ( theTime . strftime ( "%a, %d %b %Y %H:%M:%S" ) ) ) )
+
+def make_response_dict(request,d={}):
+
+ if request.user.is_authenticated():
+ d['user'] = request.user
+
+ ctx = RequestContext(request,d,[context_processors.theme])
+ print repr(ctx['theme'])
+ return ctx
+
+def json_response(data,request=None):
+ response_data = None
+ if request and request.GET.has_key('callback'):
+ callback = request.GET['callback']
+ json = simplejson.dumps(data)
+ response_data = "%s(%s)" % (callback, json)
+ else:
+ response_data = simplejson.dumps(data)
+ r = HttpResponse(response_data,content_type='application/json')
+ r['Cache-Control'] = 'no-cache, must-revalidate'
+ r['Pragma'] = 'no-cache'
+
+ return r
+
+def render403(message="You don't seem to have enough rights for what you are trying to do....",dict={}):
+ dict['message'] = message
+ return HttpResponseForbidden(loader.render_to_string("403.html",dict))
+
+def respond_to(request, template_mapping, dict={}, suffix_mapping=default_suffix_mapping):
+ accept = _accept_types(request, suffix_mapping)
+ if accept is None:
+ accept = (request.META['HTTP_ACCEPT'].split(','))[0]
+ content_type = mimeparse.best_match(template_mapping.keys(), accept)
+ template = None
+ if template_mapping.has_key(content_type):
+ template = template_mapping[content_type]
+ else:
+ template = template_mapping["text/html"]
+ if callable(template):
+ response = template(make_response_dict(request,dict))
+ elif isinstance(template, HttpResponse):
+ response = template
+ response['Content-Type'] = "%s; charset=%s" % (content_type, settings.DEFAULT_CHARSET)
+ else:
+ response = render_to_response(template,make_response_dict(request,dict))
+ response['Content-Type'] = "%s; charset=%s" % (content_type, settings.DEFAULT_CHARSET)
+ return response
+
+def redirect_to(path):
+ return HttpResponseRedirect(path) \ No newline at end of file
diff --git a/meetingtools/settings.py b/meetingtools/settings.py
new file mode 100644
index 0000000..8e72dca
--- /dev/null
+++ b/meetingtools/settings.py
@@ -0,0 +1,147 @@
+# Django settings for meetingtools project.
+from django.conf.global_settings import TEMPLATE_CONTEXT_PROCESSORS
+
+import meetingtools.site_logging
+import os
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = (
+ # ('Your Name', 'your_email@domain.com'),
+)
+
+SRC_DIR = os.path.dirname(os.path.abspath(__file__))
+BASE_DIR = os.path.abspath(os.path.join(SRC_DIR, '..'))
+
+MANAGERS = ADMINS
+
+LOCK_DIR = "/var/lock"
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
+ 'NAME': '%s/db/sqlite.db' % BASE_DIR, # Or path to database file if using sqlite3.
+ 'USER': '', # Not used with sqlite3.
+ 'PASSWORD': '', # Not used with sqlite3.
+ 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
+ 'PORT': '', # Set to empty string for default. Not used with sqlite3.
+ }
+}
+
+GRACE = 10
+IMPORT_TTL = 30
+DEFAULT_TEMPLATE_SCO=18807
+APPEND_SLASH = False
+
+# Local time zone for this installation. Choices can be found here:
+# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
+# although not all choices may be available on all operating systems.
+# On Unix systems, a value of None will cause Django to use the same
+# timezone as the operating system.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = 'Europe/Stockholm'
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+LANGUAGE_CODE = 'en-us'
+
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# If you set this to False, Django will not format dates, numbers and
+# calendars according to the current locale
+USE_L10N = True
+
+STATIC_ROOT = "%s/static" % BASE_DIR
+STATIC_URL = "/static/"
+
+LOGIN_URL = "/accounts/login"
+LOGOUT_URL = "/accounts/logout"
+DEFAULT_URL = "/"
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = 'tz78l!c=cl2=jic5$2#(bq)7-4s1ivtm*a+q0w1yi0$)hrmc7l'
+
+SESSION_ENGINE = "django.contrib.sessions.backends.file"
+#SESSION_ENGINE = "django.contrib.sessions.backends.cache"
+SESSION_FILE_PATH = "/tmp"
+SESSION_EXPIRE_AT_BROWSER_CLOSE = True
+
+CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
+ 'LOCATION': 'unique-snowflake'
+ }
+}
+
+THEMES = {
+ '__default__': {'base': "%s/themes/default" % STATIC_URL },
+ 'meetingtools.nordu.net': {'base': "%s/themes/meetingtools.nordu.net" % STATIC_URL}
+}
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+ 'django.template.loaders.filesystem.Loader',
+ 'django.template.loaders.app_directories.Loader',
+# 'django.template.loaders.eggs.Loader',
+)
+
+MIDDLEWARE_CLASSES = (
+ 'django.middleware.common.CommonMiddleware',
+ 'meetingtools.urlmiddleware.UrlMiddleware',
+ 'meetingtools.django-crossdomainxhr-middleware.XsSharing',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ #'django.contrib.auth.middleware.RemoteUserMiddleware'
+)
+
+AUTHENTICATION_BACKENDS = (
+ 'django.contrib.auth.backends.RemoteUserBackend',
+ 'django.contrib.auth.backends.ModelBackend',
+)
+
+ROOT_URLCONF = 'meetingtools.urls'
+
+TEMPLATE_DIRS = (
+ # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
+ # Always use forward slashes, even on Windows.
+ # Don't forget to use absolute paths, not relative paths.
+ "%s/templates" % BASE_DIR
+)
+
+INSTALLED_APPS = (
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+ 'django.contrib.admin',
+ 'django.contrib.humanize',
+ 'django_extensions',
+ 'south',
+ 'djcelery',
+ 'ghettoq',
+ 'djkombu',
+ 'django_co_connector',
+ 'django_co_acls',
+ 'tagging',
+ 'meetingtools.extensions',
+ 'meetingtools.apps.auth',
+ 'meetingtools.apps.room',
+ 'meetingtools.apps.cluster',
+ 'meetingtools.apps.userprofile',
+ 'meetingtools.apps.stats'
+)
+
+CARROT_BACKEND = "django"
+
+import djcelery
+djcelery.setup_loader()
+
+NOREPLY = "no-reply@sunet.se"
+AUTH_PROFILE_MODULE = "userprofile.UserProfile"
diff --git a/meetingtools/site_logging.py b/meetingtools/site_logging.py
new file mode 100644
index 0000000..cdbe3c2
--- /dev/null
+++ b/meetingtools/site_logging.py
@@ -0,0 +1,10 @@
+import logging
+import sys
+
+logger = logging.getLogger('')
+logger.setLevel(logging.DEBUG)
+handler = logging.StreamHandler(sys.stderr)
+handler.setLevel(logging.DEBUG)
+formatter = logging.Formatter('%(levelname)-8s %(message)s')
+handler.setFormatter(formatter)
+logger.addHandler(handler)
diff --git a/meetingtools/urlmiddleware.py b/meetingtools/urlmiddleware.py
new file mode 100644
index 0000000..3d77caa
--- /dev/null
+++ b/meetingtools/urlmiddleware.py
@@ -0,0 +1,126 @@
+"""
+URL Middleware
+Stefano J. Attardi (attardi.org)
+
+$Id$
+$URL$
+
+Cleans up urls by adding/removing trailing slashes, adding/removing
+the www. prefix, and allowing the language to be set from the url.
+
+If APPEND_SLASH is set to False, trailing slashes are removed from the
+urls, except for urls which have an explicit trailing slash in
+urls.py, in which case a trailing slash is added.
+
+If REMOVE_WWW is set to True, the www. prefix is removed.
+
+Finally, ?lang=xx can be appended to any url to override the default
+language setting. This override is remembered for the following
+requests. For example, /article?lang=it would show the article in
+Italian regardless of brower settings or cookies, and any following
+request to the site would be shown in Italian by default.
+
+Changelog
+
+1.3.2
+Fixed an indentation issue. Added a check for those backends
+which set an empty path (e.g. runfcgi).
+
+1.3.1
+Added support for running in a test
+suite (doesn't assume that HTTP_HOST is set)
+
+1.3
+Only use sessions for the language preference if the session
+cookie has already been set (regardless of whether session middleware
+is active). Otherwise use the plain django_language cookie.
+Only import the FlatPages module if it is active.
+
+1.2
+Added support for FlatPages.
+Switched to Django's resolve function (with workaround for when it
+returns None).
+
+1.1
+Various bugfixes.
+
+1.0
+First release.
+"""
+__version__ = "1.3.2"
+__license__ = "Python"
+__copyright__ = "Copyright (C) 2006-2007, Stefano J. Attardi"
+__author__ = "Stefano J. Attardi <http://attardi.org/>"
+__contributors__ = ["Antonio Cavedoni <http://cavedoni.com/>"]
+
+from django.conf import settings
+from django.http import HttpResponseRedirect, Http404
+from django.core.urlresolvers import resolve
+from django.utils.translation import check_for_language
+import os
+
+class UrlMiddleware:
+
+ def process_request(self, request):
+
+ # Change the language setting for the current page
+ if "lang" in request.GET and check_for_language(request.GET["lang"]):
+ if hasattr(request, "session"):
+ request.session["django_language"] = request.GET["lang"]
+ else:
+ request.COOKIES["django_language"] = request.GET["lang"]
+
+ # work-around for runfcgi
+ if request.path == "": request.path = "/"
+ request.path = '/'+request.path.lstrip('/')
+
+ # Check for a redirect based on settings.APPEND_SLASH and settings.PREPEND_WWW
+ old_url = [request.META.get("HTTP_HOST", "localhost"), request.path]
+ new_url = old_url[:]
+
+ # if REMOVE_WWW is True, remove the www. from the urls if necessary
+ if hasattr(settings, "REMOVE_WWW") and settings.REMOVE_WWW and old_url[0].startswith("www."):
+ new_url[0] = old_url[0][4:]
+
+ if hasattr(settings, "APPEND_SLASH") and not settings.APPEND_SLASH:
+ # if the url is not found, try with(out) the trailing slash
+ if old_url[1] != "/" and not self._urlExists(old_url[1]):
+
+ if old_url[1][-1] == "/":
+ other = old_url[1][:-1]
+ else:
+ other = old_url[1] + "/"
+
+ if self._urlExists(other):
+ new_url[1] = other
+
+ if new_url != old_url:
+ # Redirect
+ newurl = "%s://%s%s" % (os.environ.get("HTTPS") == "on" and "https" or "http", new_url[0], new_url[1])
+ if request.GET:
+ newurl += "?" + request.GET.urlencode()
+
+ return HttpResponseRedirect(newurl)
+
+ return None
+
+ def process_response(self, request, response):
+
+ # Change the language setting for future pages
+ if "lang" in request.GET and check_for_language(request.GET["lang"]):
+ if "sessionid" in request.COOKIES:
+ request.session["django_language"] = request.GET["lang"]
+ else:
+ response.set_cookie("django_language", request.GET["lang"])
+
+ return response
+
+ def _urlExists(self, path):
+ try:
+ if resolve(path) is None: raise Http404 # None?!? You mean 404...
+ return True
+ except Http404:
+ # check for flatpages
+ if "django.contrib.flatpages.middleware.FlatpageFallbackMiddleware" in settings.MIDDLEWARE_CLASSES:
+ from django.contrib.flatpages.models import FlatPage
+ return FlatPage.objects.filter(url=path, sites__id=settings.SITE_ID).count() == 1
diff --git a/meetingtools/urls.py b/meetingtools/urls.py
new file mode 100644
index 0000000..1c42d08
--- /dev/null
+++ b/meetingtools/urls.py
@@ -0,0 +1,53 @@
+from django.conf.urls.defaults import patterns,include
+
+# Uncomment the next two lines to enable the admin:
+from django.contrib import admin
+from meetingtools.settings import STATIC_ROOT
+from meetingtools.multiresponse import redirect_to
+from meetingtools.apps.room.feeds import RoomAtomTagFeed,RoomRSSTagField,\
+ AtomRecordingFeed, RSSRecordingField
+admin.autodiscover()
+
+def welcome(request):
+ return redirect_to('/rooms')
+
+urlpatterns = patterns('',
+ (r'^$',welcome),
+ (r'^admin/', include(admin.site.urls)),
+ (r'^static/(?P<path>.*)$','django.views.static.serve',{'document_root': STATIC_ROOT}),
+ # Login/Logout
+ (r'^accounts/login?$','meetingtools.apps.auth.views.login'),
+ (r'^accounts/login-federated$','meetingtools.apps.auth.views.accounts_login_federated'),
+ (r'^accounts/logout$','meetingtools.apps.auth.views.logout'),
+ (r'^user/?(.*)$','meetingtools.apps.room.views.list_rooms'),
+ (r'^(?:room|rooms)$','meetingtools.apps.room.views.list_rooms'),
+ (r'^go/(\d+)$','meetingtools.apps.room.views.go_by_id'),
+ (r'^go/(.+)$','meetingtools.apps.room.views.go_by_path'),
+ (r'^launch/(\d+)$','meetingtools.apps.room.views.launch'),
+ (r'^promote/(\d+)$','meetingtools.apps.room.views.promote_and_launch'),
+ (r'^room/create$','meetingtools.apps.room.views.create'),
+ (r'^room/(\d+)$','meetingtools.apps.room.views.view'),
+ (r'^room/(\d+)/modify$','meetingtools.apps.room.views.update'),
+ (r'^room/(\d+)/delete$','meetingtools.apps.room.views.delete'),
+ (r'^room/(\d+)/unlock$','meetingtools.apps.room.views.unlock'),
+ (r'^room/(\d+)/tag$','meetingtools.apps.room.views.tag'),
+ (r'^room/(\d+)/untag/(.+)$','meetingtools.apps.room.views.untag'),
+ (r'^room/(\d+)/recordings$','meetingtools.apps.room.views.recordings'),
+ (r'^room/\+(.+)\.(?:json|html|htm)$','meetingtools.apps.room.views.list_by_tag'),
+ (r'^room/\+(.+)\.(?:atom)$',RoomAtomTagFeed()),
+ (r'^room/\+(.+)\.(?:rss)$',RoomRSSTagField()),
+ (r'^room/(\d+)/recordings\.(?:atom)$',AtomRecordingFeed()),
+ (r'^room/(\d+)/recordings\.(?:rss)$',RSSRecordingField()),
+ (r'^room/\+(.+)$','meetingtools.apps.room.views.list_by_tag'),
+ (r'^widget/?\+?(.*)$','meetingtools.apps.room.views.widget'),
+ # Uncomment the admin/doc line below to enable admin documentation:
+ # (r'^admin/doc/', include('django.contrib.admindocs.urls')),
+ (r'^api/stats/user/(.*)$','meetingtools.apps.stats.views.user_minutes_api'),
+ (r'^api/stats/domain/(.+)$','meetingtools.apps.stats.views.domain_minutes_api'),
+ (r'^api/stats/room/(\d+)$','meetingtools.apps.stats.views.room_minutes_api'),
+ (r'^api/room/(\d+)/occupation$','meetingtools.apps.room.views.occupation'),
+ (r'^stats$','meetingtools.apps.stats.views.user'),
+ (r'^stats/user/(.+)$','meetingtools.apps.stats.views.user'),
+ (r'^stats/domain/(.+)$','meetingtools.apps.stats.views.domain'),
+ (r'^stats/room/(\d+)$','meetingtools.apps.stats.views.room'),
+)
diff --git a/meetingtools/utils.py b/meetingtools/utils.py
new file mode 100644
index 0000000..eee0d80
--- /dev/null
+++ b/meetingtools/utils.py
@@ -0,0 +1,20 @@
+'''
+Created on Feb 4, 2011
+
+@author: leifj
+'''
+
+def session(request,key=None,val=None):
+ if key:
+ if val:
+ request.session[key] = val
+ return val
+ else:
+ if not request.session.has_key(key):
+ request.session[key] = None
+ return request.session[key]
+ else:
+ return request.session
+
+def base_url(request):
+ return "%s://%s/" % ({True: 'https',False:'http'}[request.is_secure()],request.get_host()) \ No newline at end of file