diff options
Diffstat (limited to 'meetingtools')
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 |