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