summaryrefslogtreecommitdiff
path: root/meetingtools/apps
diff options
context:
space:
mode:
Diffstat (limited to 'meetingtools/apps')
-rw-r--r--meetingtools/apps/__init__.py0
-rw-r--r--meetingtools/apps/auth/__init__.py79
-rw-r--r--meetingtools/apps/auth/utils.py19
-rw-r--r--meetingtools/apps/auth/views.py169
-rw-r--r--meetingtools/apps/cluster/__init__.py0
-rw-r--r--meetingtools/apps/cluster/admin.py10
-rw-r--r--meetingtools/apps/cluster/migrations/0001_initial.py45
-rw-r--r--meetingtools/apps/cluster/migrations/__init__.py0
-rw-r--r--meetingtools/apps/cluster/models.py35
-rw-r--r--meetingtools/apps/room/__init__.py0
-rw-r--r--meetingtools/apps/room/admin.py10
-rw-r--r--meetingtools/apps/room/feeds.py118
-rw-r--r--meetingtools/apps/room/forms.py82
-rw-r--r--meetingtools/apps/room/management/__init__.py1
-rw-r--r--meetingtools/apps/room/management/commands/__init__.py1
-rw-r--r--meetingtools/apps/room/management/commands/import_rooms.py11
-rw-r--r--meetingtools/apps/room/migrations/0001_initial.py120
-rw-r--r--meetingtools/apps/room/migrations/0002_auto__add_field_room_deleted_sco_id.py91
-rw-r--r--meetingtools/apps/room/migrations/__init__.py0
-rw-r--r--meetingtools/apps/room/models.py142
-rw-r--r--meetingtools/apps/room/tasks.py250
-rw-r--r--meetingtools/apps/room/views.py548
-rw-r--r--meetingtools/apps/stats/__init__.py0
-rw-r--r--meetingtools/apps/stats/forms.py11
-rw-r--r--meetingtools/apps/stats/views.py273
-rw-r--r--meetingtools/apps/userprofile/__init__.py0
-rw-r--r--meetingtools/apps/userprofile/admin.py4
-rw-r--r--meetingtools/apps/userprofile/models.py21
28 files changed, 2040 insertions, 0 deletions
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)