From 10cc03db4f36cebe35bb6fef30a05df0815df84a Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 19 Mar 2010 14:03:03 +0000 Subject: [PATCH 001/116] Added django_evolution to ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 67dc653..831e1fc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ django/ registration/ dbsettings.py +django_evolution From e059c037089edb3339271eeab09667ffced73471 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 19 Mar 2010 15:18:23 +0000 Subject: [PATCH 002/116] Fixed delete user sqls --- sso/services/wiki/__init__.py | 4 ++-- test.py | 12 ------------ 2 files changed, 2 insertions(+), 14 deletions(-) delete mode 100644 test.py diff --git a/sso/services/wiki/__init__.py b/sso/services/wiki/__init__.py index 77725b8..7504fcb 100644 --- a/sso/services/wiki/__init__.py +++ b/sso/services/wiki/__init__.py @@ -20,8 +20,8 @@ class MediawikiService(BaseService): SQL_ENABLE_USER = r"UPDATE user SET user_password = %s WHERE user_name = %s" SQL_CHECK_USER = r"SELECT user_name from user WHERE user_name = %s" - SQL_DEL_REV = r"UPDATE revision SET rev_user = (SELECT user_id FROM user WHERE user_name = 'DeletedUser'), rev_user_text = 'DeletedUser' WHERE rev_user = (SELECT user_id FROM user WHERE user_name = '%s')" - SQL_DEL_USER = r"DELETE FROM USER WHERE user_name = '%s'" + SQL_DEL_REV = r"UPDATE revision SET rev_user = (SELECT user_id FROM user WHERE user_name = 'DeletedUser'), rev_user_text = 'DeletedUser' WHERE rev_user = (SELECT user_id FROM user WHERE user_name = %s)" + SQL_DEL_USER = r"DELETE FROM USER WHERE user_name = %s" def __init__(self): diff --git a/test.py b/test.py deleted file mode 100644 index 2d62731..0000000 --- a/test.py +++ /dev/null @@ -1,12 +0,0 @@ -import os -os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' - -from eve_api.cron import UpdateAPIs - -b = UpdateAPIs() -b.job() - -from sso.cron import RemoveInvalidUsers - -b = RemoveInvalidUsers() -b.job() From 45f4808148f18fb2803dee97e2dc95af46d28a30 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Mon, 22 Mar 2010 14:00:02 +0000 Subject: [PATCH 003/116] Added mining buddy service --- sso/services/miningbuddy/__init__.py | 89 ++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 sso/services/miningbuddy/__init__.py diff --git a/sso/services/miningbuddy/__init__.py b/sso/services/miningbuddy/__init__.py new file mode 100644 index 0000000..decbe17 --- /dev/null +++ b/sso/services/miningbuddy/__init__.py @@ -0,0 +1,89 @@ +import crypt +import random +from django.db import load_backend, transaction +from sso.services import BaseService +import settings + +class MiningBuddyService(BaseService): + """ + Mining Buddy Class, allows registration and sign-in + + """ + + settings = { 'require_user': False, + 'require_password': False, + 'provide_login': False } + + + SQL_ADD_USER = r"INSERT INTO users (username, password, email, emailvalid, confirmed) VALUES (%s, %s, '', 0, 1)" + SQL_ADD_API = r"INSERT INTO api_keys (userid, time, apiID, apiKey, api_valid) values (%s, %s, %s, %s, 1) + SQL_DIS_USER = r"UPDATE users SET canLogin = 0 WHERE username = %s" + SQL_ENABLE_USER = r"UPDATE users SET canLogin = 1 WHERE username = %s" + SQL_CHECK_USER = r"SELECT username from users WHERE username = %s and deleted = 0" + SQL_DEL_USER = r"UPDATE users set deleted = 1 WHERE username = %s" + + def __init__(self): + + # Use the master DB settings, bar the database name + backend = load_backend(settings.DATABASE_ENGINE) + self._db = backend.DatabaseWrapper({ + 'DATABASE_HOST': settings.DATABASE_HOST, + 'DATABASE_NAME': settings.MINING_DATABASE, + 'DATABASE_OPTIONS': {}, + 'DATABASE_PASSWORD': settings.DATABASE_PASSWORD, + 'DATABASE_PORT': settings.DATABASE_PORT, + 'DATABASE_USER': settings.DATABASE_USER, + 'TIME_ZONE': settings.TIME_ZONE,}) + + self._dbcursor = self._db.cursor() + + def _gen_salt(self): + return settings.MINING_SALT + + def _gen_mb_hash(self, password, salt=None): + if not salt: + salt = _gen_salt() + return crypt.crypt(password, salt) + + def _clean_username(self, username): + username = username.strip() + return username[0].upper() + username[1:] + + def add_user(self, username, password): + """ Add a user """ + pwhash = self._gen_mb_hash(password) + self._dbcursor.execute(self.SQL_ADD_USER, [self._clean_username(username), pwhash]) + self._db.connection.commit() + return self._clean_username(username) + + def check_user(self, username): + """ Check if the username exists """ + self._dbcursor.execute(self.SQL_CHECK_USER, [self._clean_username(username)]) + row = self._dbcursor.fetchone() + if row: + return True + return False + + def delete_user(self, uid): + """ Delete a user """ + self._dbcursor.execute(self.SQL_DEL_USER, [uid]) + self._db.connection.commit() + + def disable_user(self, uid): + """ Disable a user """ + self._dbcursor.execute(self.SQL_DIS_USER, [uid]) + self._db.connection.commit() + + def enable_user(self, uid, password): + """ Enable a user """ + pwhash = self._gen_mw_hash(password) + self._dbcursor.execute(self.SQL_ENABLE_USER, [pwhash, uid]) + self._db.connection.commit() + return True + + def reset_password(self, uid, password): + """ Reset the user's password """ + return self.enable_user(uid, password) + + +ServiceClass = 'MiningBuddyService' From 6b474b1aac60213725fbcb0e7498184e2fcd2738 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Mon, 22 Mar 2010 14:00:46 +0000 Subject: [PATCH 004/116] Syntax error --- sso/services/miningbuddy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sso/services/miningbuddy/__init__.py b/sso/services/miningbuddy/__init__.py index decbe17..0d3e107 100644 --- a/sso/services/miningbuddy/__init__.py +++ b/sso/services/miningbuddy/__init__.py @@ -16,7 +16,7 @@ class MiningBuddyService(BaseService): SQL_ADD_USER = r"INSERT INTO users (username, password, email, emailvalid, confirmed) VALUES (%s, %s, '', 0, 1)" - SQL_ADD_API = r"INSERT INTO api_keys (userid, time, apiID, apiKey, api_valid) values (%s, %s, %s, %s, 1) + SQL_ADD_API = r"INSERT INTO api_keys (userid, time, apiID, apiKey, api_valid) values (%s, %s, %s, %s, 1)" SQL_DIS_USER = r"UPDATE users SET canLogin = 0 WHERE username = %s" SQL_ENABLE_USER = r"UPDATE users SET canLogin = 1 WHERE username = %s" SQL_CHECK_USER = r"SELECT username from users WHERE username = %s and deleted = 0" From 3f458928e4fca67a8d646ab75a691298d30e181a Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Mon, 22 Mar 2010 14:17:07 +0000 Subject: [PATCH 005/116] Now provides user, character, eveapi, and reddit accounts to the Service API add_user --- sso/models.py | 4 +++- sso/services/__init__.py | 2 +- sso/services/jabber/__init__.py | 2 +- sso/services/mumble/__init__.py | 2 +- sso/services/wiki/__init__.py | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/sso/models.py b/sso/models.py index fc533c2..74e837e 100644 --- a/sso/models.py +++ b/sso/models.py @@ -121,7 +121,9 @@ class ServiceAccount(models.Model): # Create a account if we've not got a UID if self.active: if not api.check_user(self.username): - self.service_uid = api.add_user(self.username, self.password) + eve_api = EVEAccount.objects.filter(user=self.user) + reddit = RedditAccount.objects.filter(user=self.user) + self.service_uid = api.add_user(self.username, self.password, user=self.user, character=self.character, eveapi=eve_api, reddit=reddit) else: raise ExistingUser('Username %s has already been took' % self.username) else: diff --git a/sso/services/__init__.py b/sso/services/__init__.py index 66f30bd..b7a3a76 100644 --- a/sso/services/__init__.py +++ b/sso/services/__init__.py @@ -30,7 +30,7 @@ class BaseService(): 'require_password': True, 'provide_login': False } - def add_user(self, username, password): + def add_user(self, username, password, **kwargs): """ Add a user, returns a UID for that user """ return username diff --git a/sso/services/jabber/__init__.py b/sso/services/jabber/__init__.py index de63028..0566f4c 100644 --- a/sso/services/jabber/__init__.py +++ b/sso/services/jabber/__init__.py @@ -21,7 +21,7 @@ class JabberService(BaseService): self.method = "cmd" self.ejctl = eJabberdCtl(sudo=settings.JABBER_SUDO) - def add_user(self, username, password): + def add_user(self, username, password, **kwargs): """ Add user to service """ if self.method == "xmpp": if self.jabberadmin.adduser('%s@%s' % (username, settings.JABBER_SERVER), password): diff --git a/sso/services/mumble/__init__.py b/sso/services/mumble/__init__.py index 10a0ddf..7cbfc33 100644 --- a/sso/services/mumble/__init__.py +++ b/sso/services/mumble/__init__.py @@ -12,7 +12,7 @@ class MumbleService(BaseService): def _get_server(self): return Mumble.objects.get(id=settings.MUMBLE_SERVER_ID) - def add_user(self, username, password): + def add_user(self, username, password, **kwargs): """ Add a user, returns a UID for that user """ mumbleuser = MumbleUser() mumbleuser.name = username diff --git a/sso/services/wiki/__init__.py b/sso/services/wiki/__init__.py index 7504fcb..2e66c0e 100644 --- a/sso/services/wiki/__init__.py +++ b/sso/services/wiki/__init__.py @@ -51,7 +51,7 @@ class MediawikiService(BaseService): username = username.strip() return username[0].upper() + username[1:] - def add_user(self, username, password): + def add_user(self, username, password, **kwargs): """ Add a user """ pwhash = self._gen_mw_hash(password) self._dbcursor.execute(self.SQL_ADD_USER, [self._clean_username(username), pwhash]) From d54920a1b84d2cbfaae0f807ce2119a74ffd34cd Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Mon, 22 Mar 2010 15:03:23 +0000 Subject: [PATCH 006/116] Further validation and bugfixes for miningbuddy service --- settings.py | 15 ++++++++++++--- sso/models.py | 11 +++++++++-- sso/services/miningbuddy/__init__.py | 17 +++++++++++------ 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/settings.py b/settings.py index 6f0a0f0..1d4262b 100644 --- a/settings.py +++ b/settings.py @@ -90,6 +90,10 @@ AUTH_PROFILE_MODULE = 'sso.SSOUser' LOGIN_REDIRECT_URL = "/profile" LOGIN_URL = "/login" +FORCE_SCRIPT_NAME="" +DEFAULT_FROM_EMAIL = "bot@auth.dredd.it" +ACCOUNT_ACTIVATION_DAYS = 14 + ### Jabber Service Settings # Vhost to add users to @@ -115,9 +119,14 @@ MUMBLE_SERVER_ID = 1 ### Wiki Service Settings +# Mediawiki database name WIKI_DATABASE = 'dreddit_wiki' -FORCE_SCRIPT_NAME="" -DEFAULT_FROM_EMAIL = "bot@auth.dredd.it" +### Mining Buddy Settings + +# Mining Buddy database name +MINING_DATABASE = 'dreddit_mining' + +# Mining buddy secret key (in the config) +MINING_SALT = 's98ss7fsc7fd2rf62ctcrlwztstnzve9toezexcsdhfgviuinusxcdtsvbrg' -ACCOUNT_ACTIVATION_DAYS = 14 diff --git a/sso/models.py b/sso/models.py index 74e837e..5e2eba8 100644 --- a/sso/models.py +++ b/sso/models.py @@ -5,6 +5,7 @@ from django.db import models from django.db.models import signals from django.contrib.auth.models import User, UserManager, Group from eve_api.models import EVEAccount, EVEPlayerCorporation +from reddit.models import RedditAccount from services import get_api @@ -121,9 +122,15 @@ class ServiceAccount(models.Model): # Create a account if we've not got a UID if self.active: if not api.check_user(self.username): - eve_api = EVEAccount.objects.filter(user=self.user) + eveapi = None + for eacc in EVEAccount.objects.filter(user=self.user): + if self.character in eacc.characters.all(): + eveapi = eacc + break + + print eveapi reddit = RedditAccount.objects.filter(user=self.user) - self.service_uid = api.add_user(self.username, self.password, user=self.user, character=self.character, eveapi=eve_api, reddit=reddit) + self.service_uid = api.add_user(self.username, self.password, user=self.user, character=self.character, eveapi=eveapi, reddit=reddit) else: raise ExistingUser('Username %s has already been took' % self.username) else: diff --git a/sso/services/miningbuddy/__init__.py b/sso/services/miningbuddy/__init__.py index 0d3e107..bb570ae 100644 --- a/sso/services/miningbuddy/__init__.py +++ b/sso/services/miningbuddy/__init__.py @@ -15,10 +15,10 @@ class MiningBuddyService(BaseService): 'provide_login': False } - SQL_ADD_USER = r"INSERT INTO users (username, password, email, emailvalid, confirmed) VALUES (%s, %s, '', 0, 1)" + SQL_ADD_USER = r"INSERT INTO users (username, password, email, emailvalid, confirmed) VALUES (%s, %s, %s, 0, 1)" SQL_ADD_API = r"INSERT INTO api_keys (userid, time, apiID, apiKey, api_valid) values (%s, %s, %s, %s, 1)" SQL_DIS_USER = r"UPDATE users SET canLogin = 0 WHERE username = %s" - SQL_ENABLE_USER = r"UPDATE users SET canLogin = 1 WHERE username = %s" + SQL_ENABLE_USER = r"UPDATE users SET canLogin = 1, password = %s WHERE username = %s" SQL_CHECK_USER = r"SELECT username from users WHERE username = %s and deleted = 0" SQL_DEL_USER = r"UPDATE users set deleted = 1 WHERE username = %s" @@ -42,17 +42,22 @@ class MiningBuddyService(BaseService): def _gen_mb_hash(self, password, salt=None): if not salt: - salt = _gen_salt() + salt = self._gen_salt() return crypt.crypt(password, salt) def _clean_username(self, username): username = username.strip() return username[0].upper() + username[1:] - def add_user(self, username, password): + def add_user(self, username, password, **kwargs): """ Add a user """ pwhash = self._gen_mb_hash(password) - self._dbcursor.execute(self.SQL_ADD_USER, [self._clean_username(username), pwhash]) + if 'user' in kwargs: + email = kwargs['user'].email + else: + email = '' + + self._dbcursor.execute(self.SQL_ADD_USER, [self._clean_username(username), pwhash, email]) self._db.connection.commit() return self._clean_username(username) @@ -76,7 +81,7 @@ class MiningBuddyService(BaseService): def enable_user(self, uid, password): """ Enable a user """ - pwhash = self._gen_mw_hash(password) + pwhash = self._gen_mb_hash(password) self._dbcursor.execute(self.SQL_ENABLE_USER, [pwhash, uid]) self._db.connection.commit() return True From 8a2bd17e0ee06c996d316d5edbe68f47c9d78fec Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Mon, 22 Mar 2010 15:44:02 +0000 Subject: [PATCH 007/116] Enable passing of API information to MiningBuddy, cleaned out some debug statements --- sso/models.py | 2 -- sso/services/miningbuddy/__init__.py | 9 ++++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/sso/models.py b/sso/models.py index 5e2eba8..ccb0376 100644 --- a/sso/models.py +++ b/sso/models.py @@ -68,7 +68,6 @@ class SSOUser(models.Model): @staticmethod def create_user_profile(sender, instance, created, **kwargs): - print 'trigger', instance if created: profile, created = SSOUser.objects.get_or_create(user=instance) @@ -128,7 +127,6 @@ class ServiceAccount(models.Model): eveapi = eacc break - print eveapi reddit = RedditAccount.objects.filter(user=self.user) self.service_uid = api.add_user(self.username, self.password, user=self.user, character=self.character, eveapi=eveapi, reddit=reddit) else: diff --git a/sso/services/miningbuddy/__init__.py b/sso/services/miningbuddy/__init__.py index bb570ae..86784bd 100644 --- a/sso/services/miningbuddy/__init__.py +++ b/sso/services/miningbuddy/__init__.py @@ -1,5 +1,6 @@ import crypt import random +import time from django.db import load_backend, transaction from sso.services import BaseService import settings @@ -16,7 +17,7 @@ class MiningBuddyService(BaseService): SQL_ADD_USER = r"INSERT INTO users (username, password, email, emailvalid, confirmed) VALUES (%s, %s, %s, 0, 1)" - SQL_ADD_API = r"INSERT INTO api_keys (userid, time, apiID, apiKey, api_valid) values (%s, %s, %s, %s, 1)" + SQL_ADD_API = r"INSERT INTO api_keys (userid, time, apiID, apiKey, api_valid, charid) values (%s, %s, %s, %s, 1, %s)" SQL_DIS_USER = r"UPDATE users SET canLogin = 0 WHERE username = %s" SQL_ENABLE_USER = r"UPDATE users SET canLogin = 1, password = %s WHERE username = %s" SQL_CHECK_USER = r"SELECT username from users WHERE username = %s and deleted = 0" @@ -59,6 +60,12 @@ class MiningBuddyService(BaseService): self._dbcursor.execute(self.SQL_ADD_USER, [self._clean_username(username), pwhash, email]) self._db.connection.commit() + + userid = self._dbcursor.lastrowid + if 'eveapi' in kwargs: + self._dbcursor.execute(self.SQL_ADD_API, [userid, int(time.time()), kwargs['eveapi'].api_user_id, kwargs['eveapi'].api_key, kwargs['character'].id]) + self._db.connection.commit() + return self._clean_username(username) def check_user(self, username): From 484a459987e0173bbb82f416f5faded4a58c0ab8 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Tue, 23 Mar 2010 09:43:04 +0000 Subject: [PATCH 008/116] Removed django_cron dependancy, use manual started cronjobs --- cronjobs.txt | 4 + django_cron/.svn/all-wcprops | 35 ---- django_cron/.svn/entries | 198 ------------------ django_cron/.svn/format | 1 - .../.svn/text-base/README.txt.svn-base | 54 ----- .../.svn/text-base/__init__.py.svn-base | 63 ------ django_cron/.svn/text-base/base.py.svn-base | 127 ----------- django_cron/.svn/text-base/models.py.svn-base | 39 ---- .../.svn/text-base/signals.py.svn-base | 26 --- django_cron/README.txt | 54 ----- django_cron/__init__.py | 63 ------ django_cron/base.py | 127 ----------- django_cron/models.py | 39 ---- django_cron/signals.py | 26 --- eve_api/api_puller/accounts.py | 2 - eve_api/cron.py | 9 +- eve_api/models/api_player.py | 2 - reddit/cron.py | 9 +- run-cron.py | 29 +++ settings.py | 1 - sso/cron.py | 6 +- 21 files changed, 37 insertions(+), 877 deletions(-) create mode 100644 cronjobs.txt delete mode 100644 django_cron/.svn/all-wcprops delete mode 100644 django_cron/.svn/entries delete mode 100644 django_cron/.svn/format delete mode 100644 django_cron/.svn/text-base/README.txt.svn-base delete mode 100644 django_cron/.svn/text-base/__init__.py.svn-base delete mode 100644 django_cron/.svn/text-base/base.py.svn-base delete mode 100644 django_cron/.svn/text-base/models.py.svn-base delete mode 100644 django_cron/.svn/text-base/signals.py.svn-base delete mode 100644 django_cron/README.txt delete mode 100644 django_cron/__init__.py delete mode 100644 django_cron/base.py delete mode 100644 django_cron/models.py delete mode 100644 django_cron/signals.py create mode 100755 run-cron.py diff --git a/cronjobs.txt b/cronjobs.txt new file mode 100644 index 0000000..bc3ef90 --- /dev/null +++ b/cronjobs.txt @@ -0,0 +1,4 @@ + +@daily ./run-cron.py reddit.cron UpdateAPIs +@daily ./run-cron.py eve_api.cron UpdateAPIs +@hourly ./run-cron.py sso.cron RemoveInvalidUsers diff --git a/django_cron/.svn/all-wcprops b/django_cron/.svn/all-wcprops deleted file mode 100644 index 0dc4d5b..0000000 --- a/django_cron/.svn/all-wcprops +++ /dev/null @@ -1,35 +0,0 @@ -K 25 -svn:wc:ra_dav:version-url -V 34 -/svn/!svn/ver/32/trunk/django_cron -END -base.py -K 25 -svn:wc:ra_dav:version-url -V 42 -/svn/!svn/ver/32/trunk/django_cron/base.py -END -__init__.py -K 25 -svn:wc:ra_dav:version-url -V 46 -/svn/!svn/ver/16/trunk/django_cron/__init__.py -END -signals.py -K 25 -svn:wc:ra_dav:version-url -V 44 -/svn/!svn/ver/8/trunk/django_cron/signals.py -END -models.py -K 25 -svn:wc:ra_dav:version-url -V 44 -/svn/!svn/ver/16/trunk/django_cron/models.py -END -README.txt -K 25 -svn:wc:ra_dav:version-url -V 45 -/svn/!svn/ver/16/trunk/django_cron/README.txt -END diff --git a/django_cron/.svn/entries b/django_cron/.svn/entries deleted file mode 100644 index 77efd1a..0000000 --- a/django_cron/.svn/entries +++ /dev/null @@ -1,198 +0,0 @@ -9 - -dir -32 -http://django-cron.googlecode.com/svn/trunk/django_cron -http://django-cron.googlecode.com/svn - - - -2009-10-28T14:21:55.477932Z -32 -jim.mixtake@gmail.com - - -svn:special svn:externals svn:needs-lock - - - - - - - - - - - -857153d3-df44-0410-aaef-ab1f86cdcd94 - -base.py -file - - - - -2010-02-20T20:20:49.000000Z -afab29654c6380b9c2ecd00fae92522c -2009-10-28T14:21:55.477932Z -32 -jim.mixtake@gmail.com - - - - - - - - - - - - - - - - - - - - - -4064 - -__init__.py -file - - - - -2010-02-20T20:20:49.000000Z -8492cf50017e7eff784fdfbe0cc8b3ba -2009-06-02T13:59:46.682354Z -16 -Jim.mixtake - - - - - - - - - - - - - - - - - - - - - -2566 - -signals.py -file - - - - -2010-02-20T20:20:49.000000Z -8e3eba63f720bd6d830fc89d7d6be693 -2008-10-26T12:02:48.705921Z -5 -digitalxero - - - - - - - - - - - - - - - - - - - - - -1192 - -models.py -file - - - - -2010-02-20T20:20:49.000000Z -329f05f78f78465d4acfba95c42d32f7 -2009-06-02T13:59:46.682354Z -16 -Jim.mixtake - - - - - - - - - - - - - - - - - - - - - -1632 - -README.txt -file - - - - -2010-02-20T20:20:49.000000Z -801836776103697e20d5d9a9446cb28b -2009-06-02T13:59:46.682354Z -16 -Jim.mixtake - - - - - - - - - - - - - - - - - - - - - -1953 - diff --git a/django_cron/.svn/format b/django_cron/.svn/format deleted file mode 100644 index ec63514..0000000 --- a/django_cron/.svn/format +++ /dev/null @@ -1 +0,0 @@ -9 diff --git a/django_cron/.svn/text-base/README.txt.svn-base b/django_cron/.svn/text-base/README.txt.svn-base deleted file mode 100644 index cb0320b..0000000 --- a/django_cron/.svn/text-base/README.txt.svn-base +++ /dev/null @@ -1,54 +0,0 @@ -= How to install djang-cron = - -1. Put 'django_cron' into your python path - -2. Add 'django_cron' to INSTALLED_APPS in your settings.py file - -3. Add the following code to the beginning of your urls.py file (just after the imports): - - import django_cron - django_cron.autodiscover() - - -4. Create a file called 'cron.py' inside each installed app that you want to add a recurring job to. The app must be installed via the INSTALLED_APPS in your settings.py or the autodiscover will not find it. - -=== Important note === - -If you are using mod_python, you need to make sure your server is set up to server more than one request per instance, Otherwise it will kill django-cron before the tasks get started. The specific line to look for is in your 'httpd.conf' file: - - - # THIS IS BAD!!! IT WILL CRIPPLE DJANGO-CRON - MaxRequestsPerChild 1 - - -Change it to a value that is large enough that your cron jobs will get run at least once per instance. We're working on resolving this issue without dictating your server config. - -In the meantime, django_cron is best used to execute tasks that occur relatively often (at least once an hour). Try setting MaxRequestsPerChild to 50, 100, or 200 - - # Depending on traffic, and your server config, a number between 50 and 500 is probably good - # Note: the higher this number, the more memory django is likely to use. Be careful on shared hosting - MaxRequestsPerChild 100 - - -== Example cron.py == - -from django_cron import cronScheduler, Job - -# This is a function I wrote to check a feedback email address -# and add it to our database. Replace with your own imports -from MyMailFunctions import check_feedback_mailbox - -class CheckMail(Job): - """ - Cron Job that checks the lgr users mailbox and adds any - approved senders' attachments to the db - """ - - # run every 300 seconds (5 minutes) - run_every = 300 - - def job(self): - # This will be executed every 5 minutes - check_feedback_mailbox() - -cronScheduler.register(CheckMail) diff --git a/django_cron/.svn/text-base/__init__.py.svn-base b/django_cron/.svn/text-base/__init__.py.svn-base deleted file mode 100644 index b104dda..0000000 --- a/django_cron/.svn/text-base/__init__.py.svn-base +++ /dev/null @@ -1,63 +0,0 @@ -""" -Copyright (c) 2007-2008, Dj Gilcrease -All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" -from base import Job, cronScheduler - -def autodiscover(): - """ - Auto-discover INSTALLED_APPS cron.py modules and fail silently when - not present. This forces an import on them to register any cron jobs they - may want. - """ - import imp - from django.conf import settings - - for app in settings.INSTALLED_APPS: - # For each app, we need to look for an cron.py inside that app's - # package. We can't use os.path here -- recall that modules may be - # imported different ways (think zip files) -- so we need to get - # the app's __path__ and look for cron.py on that path. - - # Step 1: find out the app's __path__ Import errors here will (and - # should) bubble up, but a missing __path__ (which is legal, but weird) - # fails silently -- apps that do weird things with __path__ might - # need to roll their own cron registration. - try: - app_path = __import__(app, {}, {}, [app.split('.')[-1]]).__path__ - except AttributeError: - continue - - # Step 2: use imp.find_module to find the app's admin.py. For some - # reason imp.find_module raises ImportError if the app can't be found - # but doesn't actually try to import the module. So skip this app if - # its admin.py doesn't exist - try: - imp.find_module('cron', app_path) - except ImportError: - continue - - # Step 3: import the app's cron file. If this has errors we want them - # to bubble up. - __import__("%s.cron" % app) - - # Step 4: once we find all the cron jobs, start the cronScheduler - cronScheduler.execute() \ No newline at end of file diff --git a/django_cron/.svn/text-base/base.py.svn-base b/django_cron/.svn/text-base/base.py.svn-base deleted file mode 100644 index dbf0c3e..0000000 --- a/django_cron/.svn/text-base/base.py.svn-base +++ /dev/null @@ -1,127 +0,0 @@ -""" -Copyright (c) 2007-2008, Dj Gilcrease -All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" -import cPickle -from threading import Timer -from datetime import datetime - -from django.dispatch import dispatcher -from django.conf import settings - -from signals import cron_done -import models - -# how often to check if jobs are ready to be run (in seconds) -# in reality if you have a multithreaded server, it may get checked -# more often that this number suggests, so keep an eye on it... -# default value: 300 seconds == 5 min -polling_frequency = getattr(settings, "CRON_POLLING_FREQUENCY", 300) - -class Job(object): - # 86400 seconds == 24 hours - run_every = 86400 - - def run(self, *args, **kwargs): - self.job() - cron_done.send(sender=self, *args, **kwargs) - - def job(self): - """ - Should be overridden (this way is cleaner, but the old way - overriding run() - will still work) - """ - pass - -class CronScheduler(object): - def register(self, job_class, *args, **kwargs): - """ - Register the given Job with the scheduler class - """ - - job_instance = job_class() - - if not isinstance(job_instance, Job): - raise TypeError("You can only register a Job not a %r" % job_class) - - job, created = models.Job.objects.get_or_create(name=str(job_instance.__class__)) - if created: - job.instance = cPickle.dumps(job_instance) - job.args = cPickle.dumps(args) - job.kwargs = cPickle.dumps(kwargs) - job.run_frequency = job_instance.run_every - job.save() - - def execute(self): - """ - Queue all Jobs for execution - """ - status, created = models.Cron.objects.get_or_create(pk=1) - - # This is important for 2 reasons: - # 1. It keeps us for running more than one instance of the - # same job at a time - # 2. It reduces the number of polling threads because they - # get killed off if they happen to check while another - # one is already executing a job (only occurs with - # multi-threaded servers) - if status.executing: - return - - status.executing = True - try: - status.save() - except: - # this will fail if you're debugging, so we want it - # to fail silently and start the timer again so we - # can pick up where we left off once debugging is done - Timer(polling_frequency, self.execute).start() - return - - jobs = models.Job.objects.all() - for job in jobs: - if job.queued: - time_delta = datetime.now() - job.last_run - if (time_delta.seconds + 86400*time_delta.days) > job.run_frequency: - inst = cPickle.loads(str(job.instance)) - args = cPickle.loads(str(job.args)) - kwargs = cPickle.loads(str(job.kwargs)) - - try: - inst.run(*args, **kwargs) - job.last_run = datetime.now() - job.save() - - except Exception: - # if the job throws an error, just remove it from - # the queue. That way we can find/fix the error and - # requeue the job manually - job.queued = False - job.save() - - status.executing = False - status.save() - - # Set up for this function to run again - Timer(polling_frequency, self.execute).start() - - -cronScheduler = CronScheduler() - diff --git a/django_cron/.svn/text-base/models.py.svn-base b/django_cron/.svn/text-base/models.py.svn-base deleted file mode 100644 index cfa6640..0000000 --- a/django_cron/.svn/text-base/models.py.svn-base +++ /dev/null @@ -1,39 +0,0 @@ -""" -Copyright (c) 2007-2008, Dj Gilcrease -All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" -from django.db import models -from datetime import datetime - -class Job(models.Model): - name = models.CharField(max_length=100) - - # time between job runs (in seconds) // default: 1 day - run_frequency = models.PositiveIntegerField(default=86400) - last_run = models.DateTimeField(default=datetime.now()) - - instance = models.TextField() - args = models.TextField() - kwargs = models.TextField() - queued = models.BooleanField(default=True) - -class Cron(models.Model): - executing = models.BooleanField(default=False) \ No newline at end of file diff --git a/django_cron/.svn/text-base/signals.py.svn-base b/django_cron/.svn/text-base/signals.py.svn-base deleted file mode 100644 index a836638..0000000 --- a/django_cron/.svn/text-base/signals.py.svn-base +++ /dev/null @@ -1,26 +0,0 @@ -""" -Copyright (c) 2007-2008, Dj Gilcrease -All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" -from django.dispatch import Signal - -cron_queued = Signal() -cron_done = Signal(providing_args=["job"]) \ No newline at end of file diff --git a/django_cron/README.txt b/django_cron/README.txt deleted file mode 100644 index cb0320b..0000000 --- a/django_cron/README.txt +++ /dev/null @@ -1,54 +0,0 @@ -= How to install djang-cron = - -1. Put 'django_cron' into your python path - -2. Add 'django_cron' to INSTALLED_APPS in your settings.py file - -3. Add the following code to the beginning of your urls.py file (just after the imports): - - import django_cron - django_cron.autodiscover() - - -4. Create a file called 'cron.py' inside each installed app that you want to add a recurring job to. The app must be installed via the INSTALLED_APPS in your settings.py or the autodiscover will not find it. - -=== Important note === - -If you are using mod_python, you need to make sure your server is set up to server more than one request per instance, Otherwise it will kill django-cron before the tasks get started. The specific line to look for is in your 'httpd.conf' file: - - - # THIS IS BAD!!! IT WILL CRIPPLE DJANGO-CRON - MaxRequestsPerChild 1 - - -Change it to a value that is large enough that your cron jobs will get run at least once per instance. We're working on resolving this issue without dictating your server config. - -In the meantime, django_cron is best used to execute tasks that occur relatively often (at least once an hour). Try setting MaxRequestsPerChild to 50, 100, or 200 - - # Depending on traffic, and your server config, a number between 50 and 500 is probably good - # Note: the higher this number, the more memory django is likely to use. Be careful on shared hosting - MaxRequestsPerChild 100 - - -== Example cron.py == - -from django_cron import cronScheduler, Job - -# This is a function I wrote to check a feedback email address -# and add it to our database. Replace with your own imports -from MyMailFunctions import check_feedback_mailbox - -class CheckMail(Job): - """ - Cron Job that checks the lgr users mailbox and adds any - approved senders' attachments to the db - """ - - # run every 300 seconds (5 minutes) - run_every = 300 - - def job(self): - # This will be executed every 5 minutes - check_feedback_mailbox() - -cronScheduler.register(CheckMail) diff --git a/django_cron/__init__.py b/django_cron/__init__.py deleted file mode 100644 index b104dda..0000000 --- a/django_cron/__init__.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -Copyright (c) 2007-2008, Dj Gilcrease -All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" -from base import Job, cronScheduler - -def autodiscover(): - """ - Auto-discover INSTALLED_APPS cron.py modules and fail silently when - not present. This forces an import on them to register any cron jobs they - may want. - """ - import imp - from django.conf import settings - - for app in settings.INSTALLED_APPS: - # For each app, we need to look for an cron.py inside that app's - # package. We can't use os.path here -- recall that modules may be - # imported different ways (think zip files) -- so we need to get - # the app's __path__ and look for cron.py on that path. - - # Step 1: find out the app's __path__ Import errors here will (and - # should) bubble up, but a missing __path__ (which is legal, but weird) - # fails silently -- apps that do weird things with __path__ might - # need to roll their own cron registration. - try: - app_path = __import__(app, {}, {}, [app.split('.')[-1]]).__path__ - except AttributeError: - continue - - # Step 2: use imp.find_module to find the app's admin.py. For some - # reason imp.find_module raises ImportError if the app can't be found - # but doesn't actually try to import the module. So skip this app if - # its admin.py doesn't exist - try: - imp.find_module('cron', app_path) - except ImportError: - continue - - # Step 3: import the app's cron file. If this has errors we want them - # to bubble up. - __import__("%s.cron" % app) - - # Step 4: once we find all the cron jobs, start the cronScheduler - cronScheduler.execute() \ No newline at end of file diff --git a/django_cron/base.py b/django_cron/base.py deleted file mode 100644 index dbf0c3e..0000000 --- a/django_cron/base.py +++ /dev/null @@ -1,127 +0,0 @@ -""" -Copyright (c) 2007-2008, Dj Gilcrease -All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" -import cPickle -from threading import Timer -from datetime import datetime - -from django.dispatch import dispatcher -from django.conf import settings - -from signals import cron_done -import models - -# how often to check if jobs are ready to be run (in seconds) -# in reality if you have a multithreaded server, it may get checked -# more often that this number suggests, so keep an eye on it... -# default value: 300 seconds == 5 min -polling_frequency = getattr(settings, "CRON_POLLING_FREQUENCY", 300) - -class Job(object): - # 86400 seconds == 24 hours - run_every = 86400 - - def run(self, *args, **kwargs): - self.job() - cron_done.send(sender=self, *args, **kwargs) - - def job(self): - """ - Should be overridden (this way is cleaner, but the old way - overriding run() - will still work) - """ - pass - -class CronScheduler(object): - def register(self, job_class, *args, **kwargs): - """ - Register the given Job with the scheduler class - """ - - job_instance = job_class() - - if not isinstance(job_instance, Job): - raise TypeError("You can only register a Job not a %r" % job_class) - - job, created = models.Job.objects.get_or_create(name=str(job_instance.__class__)) - if created: - job.instance = cPickle.dumps(job_instance) - job.args = cPickle.dumps(args) - job.kwargs = cPickle.dumps(kwargs) - job.run_frequency = job_instance.run_every - job.save() - - def execute(self): - """ - Queue all Jobs for execution - """ - status, created = models.Cron.objects.get_or_create(pk=1) - - # This is important for 2 reasons: - # 1. It keeps us for running more than one instance of the - # same job at a time - # 2. It reduces the number of polling threads because they - # get killed off if they happen to check while another - # one is already executing a job (only occurs with - # multi-threaded servers) - if status.executing: - return - - status.executing = True - try: - status.save() - except: - # this will fail if you're debugging, so we want it - # to fail silently and start the timer again so we - # can pick up where we left off once debugging is done - Timer(polling_frequency, self.execute).start() - return - - jobs = models.Job.objects.all() - for job in jobs: - if job.queued: - time_delta = datetime.now() - job.last_run - if (time_delta.seconds + 86400*time_delta.days) > job.run_frequency: - inst = cPickle.loads(str(job.instance)) - args = cPickle.loads(str(job.args)) - kwargs = cPickle.loads(str(job.kwargs)) - - try: - inst.run(*args, **kwargs) - job.last_run = datetime.now() - job.save() - - except Exception: - # if the job throws an error, just remove it from - # the queue. That way we can find/fix the error and - # requeue the job manually - job.queued = False - job.save() - - status.executing = False - status.save() - - # Set up for this function to run again - Timer(polling_frequency, self.execute).start() - - -cronScheduler = CronScheduler() - diff --git a/django_cron/models.py b/django_cron/models.py deleted file mode 100644 index cfa6640..0000000 --- a/django_cron/models.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -Copyright (c) 2007-2008, Dj Gilcrease -All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" -from django.db import models -from datetime import datetime - -class Job(models.Model): - name = models.CharField(max_length=100) - - # time between job runs (in seconds) // default: 1 day - run_frequency = models.PositiveIntegerField(default=86400) - last_run = models.DateTimeField(default=datetime.now()) - - instance = models.TextField() - args = models.TextField() - kwargs = models.TextField() - queued = models.BooleanField(default=True) - -class Cron(models.Model): - executing = models.BooleanField(default=False) \ No newline at end of file diff --git a/django_cron/signals.py b/django_cron/signals.py deleted file mode 100644 index a836638..0000000 --- a/django_cron/signals.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Copyright (c) 2007-2008, Dj Gilcrease -All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" -from django.dispatch import Signal - -cron_queued = Signal() -cron_done = Signal(providing_args=["job"]) \ No newline at end of file diff --git a/eve_api/api_puller/accounts.py b/eve_api/api_puller/accounts.py index 86ac2a0..8f84d12 100755 --- a/eve_api/api_puller/accounts.py +++ b/eve_api/api_puller/accounts.py @@ -22,12 +22,10 @@ def import_eve_account(api_key, user_id): """ Imports an account from the API into the EVEAccount model. """ - print user_id, ":", api_key auth_params = {'userID': user_id, 'apiKey': api_key} account_doc = CachedDocument.objects.api_query('/account/Characters.xml.aspx', params=auth_params, no_cache=False) - #print account_doc.body dom = minidom.parseString(account_doc.body) diff --git a/eve_api/cron.py b/eve_api/cron.py index 920850e..a1d3826 100644 --- a/eve_api/cron.py +++ b/eve_api/cron.py @@ -1,18 +1,14 @@ import logging -from django_cron import cronScheduler, Job from eve_api.models.api_player import EVEAccount, EVEPlayerCorporation import eve_api.api_puller.accounts from eve_api.api_exceptions import APIAuthException, APINoUserIDException -class UpdateAPIs(Job): +class UpdateAPIs(): """ Updates all Eve API elements in the database """ - # run every 2 hours - run_every = 7200 - @property def _logger(self): if not hasattr(self, '__logger'): @@ -36,6 +32,3 @@ class UpdateAPIs(Job): for corp in EVEPlayerCorporation.objects.all(): corp.query_and_update_corp() - - -cronScheduler.register(UpdateAPIs) diff --git a/eve_api/models/api_player.py b/eve_api/models/api_player.py index a784851..b39d019 100644 --- a/eve_api/models/api_player.py +++ b/eve_api/models/api_player.py @@ -192,9 +192,7 @@ class EVEPlayerCorporation(EVEAPIModel): continue except IndexError: # Something weird has happened - print " * Index Error:", tag_map[0] continue - print "Updating", self.id, self.name self.api_last_updated = datetime.utcnow() self.save() diff --git a/reddit/cron.py b/reddit/cron.py index f078117..21ffab7 100644 --- a/reddit/cron.py +++ b/reddit/cron.py @@ -1,18 +1,13 @@ import time import logging -from django_cron import cronScheduler, Job from reddit.models import RedditAccount from reddit.api import Inbox -class UpdateAPIs(Job): +class UpdateAPIs(): """ Updates all Reddit API elements in the database """ - - # run every 24 hours - run_every = 86400 - @property def _logger(self): if not hasattr(self, '__logger'): @@ -60,5 +55,3 @@ class ProcessInbox(Job): else: print key.username - -cronScheduler.register(UpdateAPIs) diff --git a/run-cron.py b/run-cron.py new file mode 100755 index 0000000..4adf13f --- /dev/null +++ b/run-cron.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +"""Executes a Django cronjob""" + +import sys +from django.core.management import setup_environ +import settings + +setup_environ(settings) + +logging.basicConfig(level=logging.INFO) +log = logging.getLogger('runcron') + +try: + mod = __import__(sys.argv[1]) +except ImportError: + raise Exception('Error creating service') + +for i in sys.argv[1].split(".")[1:]: + mod = getattr(mod, i) +cron_class = getattr(mod, sys.argv[2])() + +log.info("Starting Job %s in %s" % (sys.argv[2], sys.argv[1]) + +try: + cron_class.job() +except: + log.error("Error executing job, aborting.") + +log.info("Job complete") diff --git a/settings.py b/settings.py index 1d4262b..249751b 100644 --- a/settings.py +++ b/settings.py @@ -75,7 +75,6 @@ INSTALLED_APPS = ( 'django.contrib.sites', 'django_evolution', 'registration', - 'django_cron', 'eve_proxy', 'eve_api', 'mumble', diff --git a/sso/cron.py b/sso/cron.py index 2a9c614..d686cfc 100644 --- a/sso/cron.py +++ b/sso/cron.py @@ -1,11 +1,10 @@ import logging -from django_cron import cronScheduler, Job from django.contrib.auth.models import User, Group from eve_api.models import EVEAccount from sso.models import ServiceAccount -class RemoveInvalidUsers(Job): +class RemoveInvalidUsers(): """ Cycles through all users, check if their permissions are correct. """ @@ -27,7 +26,7 @@ class RemoveInvalidUsers(Job): # Check each service account and delete access if they're not allowed for servacc in ServiceAccount.objects.filter(user=user): if not (set(user.groups.all()) & set(servacc.service.groups.all())): - print "User %s is not in allowed group for %s, deleting account" % (user.username, servacc.service) + self._logger.info("User %s is not in allowed group for %s, deleting account" % (user.username, servacc.service)) servacc.delete() pass @@ -39,5 +38,4 @@ class RemoveInvalidUsers(Job): pass -cronScheduler.register(RemoveInvalidUsers) From edc5def1f37e0cbe946b11fb2c36f2368e004acb Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Tue, 23 Mar 2010 09:44:46 +0000 Subject: [PATCH 009/116] Updated cronjob defaults --- cronjobs.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cronjobs.txt b/cronjobs.txt index bc3ef90..8d94013 100644 --- a/cronjobs.txt +++ b/cronjobs.txt @@ -1,4 +1,5 @@ +ROOT=$HOME/auth/auth/ -@daily ./run-cron.py reddit.cron UpdateAPIs -@daily ./run-cron.py eve_api.cron UpdateAPIs -@hourly ./run-cron.py sso.cron RemoveInvalidUsers +@daily $ROOT/run-cron.py reddit.cron UpdateAPIs +@daily $ROOT/run-cron.py eve_api.cron UpdateAPIs +@hourly $ROOT/run-cron.py sso.cron RemoveInvalidUsers From d2b47da70440043c22a7ea547c13527189d2b1d0 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Tue, 23 Mar 2010 10:00:05 +0000 Subject: [PATCH 010/116] Updated to current hg head of mumble-django --- mumble/MumbleCtlDbus.py | 13 +- mumble/MumbleCtlIce.py | 219 ++++++- mumble/admin.py | 59 +- .../mysql/01-schema-prepare-mumble.sql | 18 + .../mysql/11-data-mumble_mumbleserver.sql | 3 + .../mysql/12-data-mumble_mumble.sql | 6 + .../mysql/21-schema-cleanup-mumble.sql | 29 + .../mysql/22-schema-cleanup-mumbleuser.sql | 3 + .../pgsql/01-schema-prepare-mumble.sql | 13 + .../pgsql/11-data-mumble_mumbleserver.sql | 5 + .../pgsql/12-data-mumble_mumble.sql | 8 + .../pgsql/21-schema-cleanup-mumble.sql | 34 + .../pgsql/22-schema-cleanup-mumbleuser.sql | 5 + .../conversionsql/sqlite/01-schema-mumble.sql | 10 + .../sqlite/02-schema-mumbleuser.sql | 10 + .../sqlite/11-data-mumbleserver.sql | 3 + .../conversionsql/sqlite/12-data-mumble.sql | 11 + .../sqlite/13-data-mumbleuser.sql | 9 + .../sqlite/21-remove-old-mumble.sql | 1 + .../sqlite/22-rename-new-mumble.sql | 1 + .../sqlite/23-remove-old-mumbleuser.sql | 1 + .../sqlite/24-rename-new-mumbleuser.sql | 1 + mumble/forms.py | 324 ++++++++++ .../locale/_outdated/hr/LC_MESSAGES/django.mo | Bin 0 -> 6846 bytes .../locale/_outdated/hr/LC_MESSAGES/django.po | 580 ++++++++++++++++++ .../locale/_outdated/pl/LC_MESSAGES/django.mo | Bin 0 -> 6015 bytes .../locale/_outdated/pl/LC_MESSAGES/django.po | 574 +++++++++++++++++ mumble/locale/de/LC_MESSAGES/django.mo | Bin 0 -> 9916 bytes mumble/locale/de/LC_MESSAGES/django.po | 530 ++++++++++++++++ mumble/locale/fr/LC_MESSAGES/django.mo | Bin 0 -> 10574 bytes mumble/locale/fr/LC_MESSAGES/django.po | 568 +++++++++++++++++ mumble/locale/it/LC_MESSAGES/django.mo | Bin 0 -> 10087 bytes mumble/locale/it/LC_MESSAGES/django.po | 573 +++++++++++++++++ mumble/locale/ja/LC_MESSAGES/django.mo | Bin 0 -> 10072 bytes mumble/locale/ja/LC_MESSAGES/django.po | 543 ++++++++++++++++ mumble/management/__init__.py | 42 +- mumble/management/commands/checkenv.py | 18 +- mumble/management/commands/getslice.py | 56 ++ mumble/management/server_detect.py | 72 ++- mumble/management/update_schema.py | 76 +++ mumble/mctl.py | 94 +-- mumble/mmobjects.py | 108 +++- mumble/models.py | 459 +++++++++----- mumble/templates/mumble/channel.html | 28 + mumble/templates/mumble/list.html | 24 + mumble/templates/mumble/mobile_list.html | 21 + mumble/templates/mumble/mobile_mumble.html | 12 + mumble/templates/mumble/mumble.html | 388 ++++++++++++ mumble/templates/mumble/offline.html | 14 + mumble/templates/mumble/player.html | 35 ++ mumble/templates/mumble/server.html | 14 + mumble/templatetags/__init__.py | 14 + mumble/templatetags/mumble_extras.py | 62 ++ mumble/testrunner.py | 99 +++ mumble/tests.py | 208 +++++++ mumble/urls.py | 35 ++ mumble/views.py | 408 ++++++++++++ 57 files changed, 6102 insertions(+), 339 deletions(-) create mode 100644 mumble/conversionsql/mysql/01-schema-prepare-mumble.sql create mode 100644 mumble/conversionsql/mysql/11-data-mumble_mumbleserver.sql create mode 100644 mumble/conversionsql/mysql/12-data-mumble_mumble.sql create mode 100644 mumble/conversionsql/mysql/21-schema-cleanup-mumble.sql create mode 100644 mumble/conversionsql/mysql/22-schema-cleanup-mumbleuser.sql create mode 100644 mumble/conversionsql/pgsql/01-schema-prepare-mumble.sql create mode 100644 mumble/conversionsql/pgsql/11-data-mumble_mumbleserver.sql create mode 100644 mumble/conversionsql/pgsql/12-data-mumble_mumble.sql create mode 100644 mumble/conversionsql/pgsql/21-schema-cleanup-mumble.sql create mode 100644 mumble/conversionsql/pgsql/22-schema-cleanup-mumbleuser.sql create mode 100644 mumble/conversionsql/sqlite/01-schema-mumble.sql create mode 100644 mumble/conversionsql/sqlite/02-schema-mumbleuser.sql create mode 100644 mumble/conversionsql/sqlite/11-data-mumbleserver.sql create mode 100644 mumble/conversionsql/sqlite/12-data-mumble.sql create mode 100644 mumble/conversionsql/sqlite/13-data-mumbleuser.sql create mode 100644 mumble/conversionsql/sqlite/21-remove-old-mumble.sql create mode 100644 mumble/conversionsql/sqlite/22-rename-new-mumble.sql create mode 100644 mumble/conversionsql/sqlite/23-remove-old-mumbleuser.sql create mode 100644 mumble/conversionsql/sqlite/24-rename-new-mumbleuser.sql create mode 100644 mumble/forms.py create mode 100644 mumble/locale/_outdated/hr/LC_MESSAGES/django.mo create mode 100644 mumble/locale/_outdated/hr/LC_MESSAGES/django.po create mode 100644 mumble/locale/_outdated/pl/LC_MESSAGES/django.mo create mode 100644 mumble/locale/_outdated/pl/LC_MESSAGES/django.po create mode 100644 mumble/locale/de/LC_MESSAGES/django.mo create mode 100644 mumble/locale/de/LC_MESSAGES/django.po create mode 100644 mumble/locale/fr/LC_MESSAGES/django.mo create mode 100755 mumble/locale/fr/LC_MESSAGES/django.po create mode 100644 mumble/locale/it/LC_MESSAGES/django.mo create mode 100644 mumble/locale/it/LC_MESSAGES/django.po create mode 100644 mumble/locale/ja/LC_MESSAGES/django.mo create mode 100644 mumble/locale/ja/LC_MESSAGES/django.po create mode 100644 mumble/management/commands/getslice.py create mode 100644 mumble/management/update_schema.py create mode 100644 mumble/templates/mumble/channel.html create mode 100644 mumble/templates/mumble/list.html create mode 100644 mumble/templates/mumble/mobile_list.html create mode 100644 mumble/templates/mumble/mobile_mumble.html create mode 100644 mumble/templates/mumble/mumble.html create mode 100644 mumble/templates/mumble/offline.html create mode 100644 mumble/templates/mumble/player.html create mode 100644 mumble/templates/mumble/server.html create mode 100644 mumble/templatetags/__init__.py create mode 100644 mumble/templatetags/mumble_extras.py create mode 100644 mumble/testrunner.py create mode 100644 mumble/tests.py create mode 100644 mumble/urls.py create mode 100644 mumble/views.py diff --git a/mumble/MumbleCtlDbus.py b/mumble/MumbleCtlDbus.py index 5684d36..baa3679 100644 --- a/mumble/MumbleCtlDbus.py +++ b/mumble/MumbleCtlDbus.py @@ -69,7 +69,7 @@ class MumbleCtlDbus_118(MumbleCtlBase): info[str(key)] = conf[key]; return info; - def getConf(self, srvid, key, value): + def getConf(self, srvid, key): if key == "username": key = "playername"; @@ -125,9 +125,10 @@ class MumbleCtlDbus_118(MumbleCtlBase): ret = {}; for channel in chans: + print channel; ret[ channel[0] ] = ObjectInfo( id = int(channel[0]), - name = str(channel[1]), + name = unicode(channel[1]), parent = int(channel[2]), links = [ int(lnk) for lnk in channel[3] ], ); @@ -149,7 +150,7 @@ class MumbleCtlDbus_118(MumbleCtlBase): selfDeaf = bool( playerObj[5] ), channel = int( playerObj[6] ), userid = int( playerObj[7] ), - name = str( playerObj[8] ), + name = unicode( playerObj[8] ), onlinesecs = int( playerObj[9] ), bytespersec = int( playerObj[10] ) ); @@ -178,7 +179,7 @@ class MumbleCtlDbus_118(MumbleCtlBase): applySubs = bool(rule[1]), inherited = bool(rule[2]), userid = int(rule[3]), - group = str(rule[4]), + group = unicode(rule[4]), allow = int(rule[5]), deny = int(rule[6]), ) @@ -186,7 +187,7 @@ class MumbleCtlDbus_118(MumbleCtlBase): ]; groups = [ ObjectInfo( - name = str(group[0]), + name = unicode(group[0]), inherited = bool(group[1]), inherit = bool(group[2]), inheritable = bool(group[3]), @@ -261,7 +262,7 @@ class MumbleCtlDbus_118(MumbleCtlBase): def setTexture(self, srvid, mumbleid, infile): # open image, convert to RGBA, and resize to 600x60 - img = Image.open( infile ).convert( "RGBA" ).transform( ( 600, 60 ), Image.EXTENT, ( 0, 0, 600, 60 ) ); + img = infile.convert( "RGBA" ).transform( ( 600, 60 ), Image.EXTENT, ( 0, 0, 600, 60 ) ); # iterate over the list and pack everything into a string bgrastring = ""; for ent in list( img.getdata() ): diff --git a/mumble/MumbleCtlIce.py b/mumble/MumbleCtlIce.py index a8df6ab..b35e9fa 100644 --- a/mumble/MumbleCtlIce.py +++ b/mumble/MumbleCtlIce.py @@ -15,16 +15,20 @@ * GNU General Public License for more details. """ -from os.path import exists +from time import time +from functools import wraps +from StringIO import StringIO +from os.path import exists, join +from os import unlink, name as os_name from PIL import Image from struct import pack, unpack -from zlib import compress, decompress +from zlib import compress, decompress, error from mctl import MumbleCtlBase from utils import ObjectInfo -import Ice +import Ice, IcePy, tempfile def protectDjangoErrPage( func ): @@ -36,47 +40,104 @@ def protectDjangoErrPage( func ): non-existant files and borking. """ - def protection_wrapper( *args, **kwargs ): + @wraps(func) + def protection_wrapper( self, *args, **kwargs ): """ Call the original function and catch Ice exceptions. """ try: - return func( *args, **kwargs ); - except Ice.Exception, e: - raise e; + return func( self, *args, **kwargs ); + except Ice.Exception, err: + raise err; protection_wrapper.innerfunc = func return protection_wrapper; - @protectDjangoErrPage -def MumbleCtlIce( connstring, slicefile ): +def MumbleCtlIce( connstring, slicefile=None, icesecret=None ): """ Choose the correct Ice handler to use (1.1.8 or 1.2.x), and make sure the Murmur version matches the slice Version. + + Optional parameters are the path to the slice file and the Ice secret + necessary to authenticate to Murmur. + + The path can be omitted only if running Murmur 1.2.3 or later, which + exports a getSlice method to retrieve the Slice from. """ + prop = Ice.createProperties([]) + prop.setProperty("Ice.ImplicitContext", "Shared") + + idd = Ice.InitializationData() + idd.properties = prop + + ice = Ice.initialize(idd) + + if icesecret: + ice.getImplicitContext().put( "secret", icesecret.encode("utf-8") ) + + prx = ice.stringToProxy( connstring.encode("utf-8") ) + + try: + prx.ice_ping() + except Ice.Exception: + raise EnvironmentError( "Murmur does not appear to be listening on this address (Ice ping failed)." ) + try: import Murmur - except ImportError: - if not slicefile: - raise EnvironmentError( "You didn't configure a slice file. Please set the SLICE variable in settings.py." ) - - if not exists( slicefile ): - raise EnvironmentError( "The slice file does not exist: '%s' - please check the settings." % slicefile ) - - if " " in slicefile: - raise EnvironmentError( "You have a space char in your Slice path. This will confuse Ice, please check." ) - - if not slicefile.endswith( ".ice" ): - raise EnvironmentError( "The slice file name MUST end with '.ice'." ) - - Ice.loadSlice( slicefile ) + # Try loading the Slice from Murmur directly via its getSlice method. + # See scripts/testdynamic.py in Mumble's Git repository. + try: + slice = IcePy.Operation( 'getSlice', + Ice.OperationMode.Idempotent, Ice.OperationMode.Idempotent, + True, (), (), (), IcePy._t_string, () + ).invoke(prx, ((), None)) + except (TypeError, Ice.OperationNotExistException): + if not slicefile: + raise EnvironmentError( + "You didn't configure a slice file. Please set the SLICE variable in settings.py." ) + if not exists( slicefile ): + raise EnvironmentError( + "The slice file does not exist: '%s' - please check the settings." % slicefile ) + if " " in slicefile: + raise EnvironmentError( + "You have a space char in your Slice path. This will confuse Ice, please check." ) + if not slicefile.endswith( ".ice" ): + raise EnvironmentError( "The slice file name MUST end with '.ice'." ) + + try: + Ice.loadSlice( slicefile ) + except RuntimeError: + raise RuntimeError( "Slice preprocessing failed. Please check your server's error log." ) + else: + if os_name == "nt": + # It weren't Windows if it didn't need to be treated differently. *sigh* + temppath = join( tempfile.gettempdir(), "Murmur.ice" ) + slicetemp = open( temppath, "w+b" ) + try: + slicetemp.write( slice ) + finally: + slicetemp.close() + try: + Ice.loadSlice( temppath ) + except RuntimeError: + raise RuntimeError( "Slice preprocessing failed. Please check your server's error log." ) + finally: + unlink(temppath) + else: + slicetemp = tempfile.NamedTemporaryFile( suffix='.ice' ) + try: + slicetemp.write( slice ) + slicetemp.flush() + Ice.loadSlice( slicetemp.name ) + except RuntimeError: + raise RuntimeError( "Slice preprocessing failed. Please check your server's error log." ) + finally: + slicetemp.close() import Murmur - ice = Ice.initialize() - prx = ice.stringToProxy( connstring.encode("utf-8") ) - meta = Murmur.MetaPrx.checkedCast(prx) + meta = Murmur.MetaPrx.checkedCast(prx) murmurversion = meta.getVersion()[:3] @@ -84,7 +145,16 @@ def MumbleCtlIce( connstring, slicefile ): return MumbleCtlIce_118( connstring, meta ); elif murmurversion[:2] == (1, 2): - return MumbleCtlIce_120( connstring, meta ); + if murmurversion[2] < 2: + return MumbleCtlIce_120( connstring, meta ); + + elif murmurversion[2] == 2: + return MumbleCtlIce_122( connstring, meta ); + + elif murmurversion[2] == 3: + return MumbleCtlIce_123( connstring, meta ); + + raise NotImplementedError( "No ctl object available for Murmur version %d.%d.%d" % tuple(murmurversion) ) class MumbleCtlIce_118(MumbleCtlBase): @@ -291,7 +361,10 @@ class MumbleCtlIce_118(MumbleCtlBase): if len(texture) == 0: raise ValueError( "No Texture has been set." ); # this returns a list of bytes. - decompressed = decompress( texture ); + try: + decompressed = decompress( texture ); + except error, err: + raise ValueError( err ) # iterate over 4 byte chunks of the string imgdata = ""; for idx in range( 0, len(decompressed), 4 ): @@ -308,7 +381,7 @@ class MumbleCtlIce_118(MumbleCtlBase): @protectDjangoErrPage def setTexture(self, srvid, mumbleid, infile): # open image, convert to RGBA, and resize to 600x60 - img = Image.open( infile ).convert( "RGBA" ).transform( ( 600, 60 ), Image.EXTENT, ( 0, 0, 600, 60 ) ); + img = infile.convert( "RGBA" ).transform( ( 600, 60 ), Image.EXTENT, ( 0, 0, 600, 60 ) ); # iterate over the list and pack everything into a string bgrastring = ""; for ent in list( img.getdata() ): @@ -360,7 +433,20 @@ class MumbleCtlIce_120(MumbleCtlIce_118): @protectDjangoErrPage def getPlayers(self, srvid): - return self._getIceServerObject(srvid).getUsers(); + userdata = self._getIceServerObject(srvid).getUsers(); + for key in userdata: + if isinstance( userdata[key], str ): + userdata[key] = userdata[key].decode( "UTF-8" ) + return userdata + + @protectDjangoErrPage + def getState(self, srvid, sessionid): + userdata = self._getIceServerObject(srvid).getState(sessionid); + for key in userdata.__dict__: + attr = getattr( userdata, key ) + if isinstance( attr, str ): + setattr( userdata, key, attr.decode( "UTF-8" ) ) + return userdata @protectDjangoErrPage def registerPlayer(self, srvid, name, email, password): @@ -433,4 +519,77 @@ class MumbleCtlIce_120(MumbleCtlIce_118): @protectDjangoErrPage def setACL(self, srvid, channelid, acls, groups, inherit): return self._getIceServerObject(srvid).setACL( channelid, acls, groups, inherit ); + + @protectDjangoErrPage + def getBans(self, srvid): + return self._getIceServerObject(srvid).getBans(); + + @protectDjangoErrPage + def setBans(self, srvid, bans): + return self._getIceServerObject(srvid).setBans(bans); + + @protectDjangoErrPage + def addBanForSession(self, srvid, sessionid, **kwargs): + session = self.getState(srvid, sessionid); + if "bits" not in kwargs: + kwargs["bits"] = 128; + if "start" not in kwargs: + kwargs["start"] = int(time()); + if "duration" not in kwargs: + kwargs["duration"] = 3600; + return self.addBan(srvid, address=session.address, **kwargs); + + @protectDjangoErrPage + def addBan(self, srvid, **kwargs): + for key in kwargs: + if isinstance( kwargs[key], unicode ): + kwargs[key] = kwargs[key].encode("UTF-8") + + from Murmur import Ban + srvbans = self.getBans(srvid); + srvbans.append( Ban( **kwargs ) ); + return self.setBans(srvid, srvbans); + + @protectDjangoErrPage + def kickUser(self, srvid, userid, reason=""): + return self._getIceServerObject(srvid).kickUser( userid, reason.encode("UTF-8") ); + + +class MumbleCtlIce_122(MumbleCtlIce_120): + @protectDjangoErrPage + def getTexture(self, srvid, mumbleid): + raise ValueError( "This method is buggy in 1.2.2, sorry dude." ); + + @protectDjangoErrPage + def setTexture(self, srvid, mumbleid, infile): + buf = StringIO() + infile.save( buf, "PNG" ) + buf.seek(0) + self._getIceServerObject(srvid).setTexture(mumbleid, buf.read()) + + +class MumbleCtlIce_123(MumbleCtlIce_120): + + @protectDjangoErrPage + def getRawTexture(self, srvid, mumbleid): + return self._getIceServerObject(srvid).getTexture(mumbleid) + + @protectDjangoErrPage + def getTexture(self, srvid, mumbleid): + texture = self.getRawTexture(srvid, mumbleid) + if len(texture) == 0: + raise ValueError( "No Texture has been set." ); + from StringIO import StringIO + try: + return Image.open( StringIO( texture ) ) + except IOError, err: + raise ValueError( err ) + + @protectDjangoErrPage + def setTexture(self, srvid, mumbleid, infile): + buf = StringIO() + infile.save( buf, "PNG" ) + buf.seek(0) + self._getIceServerObject(srvid).setTexture(mumbleid, buf.read()) + diff --git a/mumble/admin.py b/mumble/admin.py index 1e437ef..5a9f6a5 100644 --- a/mumble/admin.py +++ b/mumble/admin.py @@ -14,27 +14,61 @@ * GNU General Public License for more details. """ +from django.conf import settings from django.contrib import admin from django.utils.translation import ugettext_lazy as _ -#from mumble.forms import MumbleAdminForm, MumbleUserAdminForm -from mumble.models import Mumble, MumbleUser +from mumble.forms import MumbleServerForm, MumbleAdminForm, MumbleUserAdminForm +from mumble.models import MumbleServer, Mumble, MumbleUser + +class MumbleServerAdmin(admin.ModelAdmin): + list_display = [ 'dbus', 'get_murmur_online' ] + search_fields = [ 'dbus' ] + ordering = [ 'dbus' ] + + form = MumbleServerForm + + def get_murmur_online( self, obj ): + return obj.online + + get_murmur_online.short_description = _('Master is running') + get_murmur_online.boolean = True + class MumbleAdmin(admin.ModelAdmin): """ Specification for the "Server administration" admin section. """ - list_display = [ 'name', 'addr', 'port', 'get_booted', 'get_is_public', - 'get_users_regged', 'get_users_online', 'get_channel_count' ]; - list_filter = [ 'addr' ]; + list_display = [ 'name', 'srvid', 'get_addr', 'get_port', 'get_murmur_online', 'get_booted', + 'get_is_public', 'get_users_regged', 'get_users_online', 'get_channel_count' ]; + list_filter = [ 'addr', 'server' ]; search_fields = [ 'name', 'addr', 'port' ]; ordering = [ 'name' ]; - #form = MumbleAdminForm; + form = MumbleAdminForm; + def get_murmur_online( self, obj ): + return obj.server.online + + get_murmur_online.short_description = _('Master is running') + get_murmur_online.boolean = True + + def get_addr( self, obj ): + if not obj.addr: + return "*" + return obj.addr + + get_addr.short_description = _('Server Address') + + def get_port( self, obj ): + if not obj.port: + return "< %d >" % (settings.MUMBLE_DEFAULT_PORT + obj.srvid - 1) + return obj.port + + get_port.short_description = _('Server Port') def get_booted( self, obj ): return obj.booted - get_booted.short_description = _('Boot Server') + get_booted.short_description = _('Instance is running') get_booted.boolean = True def get_users_regged( self, obj ): @@ -88,14 +122,17 @@ class MumbleUserAdmin(admin.ModelAdmin): search_fields = [ 'owner__username', 'name' ]; ordering = [ 'owner__username' ]; - #form = MumbleUserAdminForm + form = MumbleUserAdminForm def get_acl_admin( self, obj ): - return obj.aclAdmin + if obj.server.booted: + return obj.aclAdmin + return None get_acl_admin.short_description = _('Admin on root channel') get_acl_admin.boolean = True -admin.site.register( Mumble, MumbleAdmin ); -admin.site.register( MumbleUser, MumbleUserAdmin ); +admin.site.register( MumbleServer, MumbleServerAdmin ); +admin.site.register( Mumble, MumbleAdmin ); +admin.site.register( MumbleUser, MumbleUserAdmin ); diff --git a/mumble/conversionsql/mysql/01-schema-prepare-mumble.sql b/mumble/conversionsql/mysql/01-schema-prepare-mumble.sql new file mode 100644 index 0000000..6830068 --- /dev/null +++ b/mumble/conversionsql/mysql/01-schema-prepare-mumble.sql @@ -0,0 +1,18 @@ +BEGIN; +-- Model: Mumble +ALTER TABLE `mumble_mumble` + ADD `server_id` integer FIRST; +ALTER TABLE `mumble_mumble` + ADD `display` varchar(200) AFTER `name`; +ALTER TABLE `mumble_mumble` + MODIFY `port` integer NULL; +COMMIT; + +BEGIN; +ALTER TABLE mumble_mumble DROP KEY `addr`; +COMMIT; + +BEGIN; +CREATE INDEX `mumble_mumble_server_id_idx` + ON `mumble_mumble` (`server_id`); +COMMIT; diff --git a/mumble/conversionsql/mysql/11-data-mumble_mumbleserver.sql b/mumble/conversionsql/mysql/11-data-mumble_mumbleserver.sql new file mode 100644 index 0000000..110611e --- /dev/null +++ b/mumble/conversionsql/mysql/11-data-mumble_mumbleserver.sql @@ -0,0 +1,3 @@ +INSERT INTO `mumble_mumbleserver` ( `dbus`, `secret` ) +SELECT DISTINCT `dbus`, '' +FROM `mumble_mumble`; diff --git a/mumble/conversionsql/mysql/12-data-mumble_mumble.sql b/mumble/conversionsql/mysql/12-data-mumble_mumble.sql new file mode 100644 index 0000000..21cd480 --- /dev/null +++ b/mumble/conversionsql/mysql/12-data-mumble_mumble.sql @@ -0,0 +1,6 @@ +UPDATE `mumble_mumble` +SET `server_id`=( + SELECT `id` + FROM `mumble_mumbleserver` + WHERE `mumble_mumbleserver`.`dbus` = `mumble_mumble`.`dbus` + ); diff --git a/mumble/conversionsql/mysql/21-schema-cleanup-mumble.sql b/mumble/conversionsql/mysql/21-schema-cleanup-mumble.sql new file mode 100644 index 0000000..677cded --- /dev/null +++ b/mumble/conversionsql/mysql/21-schema-cleanup-mumble.sql @@ -0,0 +1,29 @@ +-- Model: Mumble +ALTER TABLE `mumble_mumble` + DROP COLUMN `dbus`; +ALTER TABLE `mumble_mumble` + DROP COLUMN `url`; +ALTER TABLE `mumble_mumble` + DROP COLUMN `motd`; +ALTER TABLE `mumble_mumble` + DROP COLUMN `passwd`; +ALTER TABLE `mumble_mumble` + DROP COLUMN `supw`; +ALTER TABLE `mumble_mumble` + DROP COLUMN `users`; +ALTER TABLE `mumble_mumble` + DROP COLUMN `bwidth`; +ALTER TABLE `mumble_mumble` + DROP COLUMN `sslcrt`; +ALTER TABLE `mumble_mumble` + DROP COLUMN `sslkey`; +ALTER TABLE `mumble_mumble` + DROP COLUMN `obfsc`; +ALTER TABLE `mumble_mumble` + DROP COLUMN `player`; +ALTER TABLE `mumble_mumble` + DROP COLUMN `channel`; +ALTER TABLE `mumble_mumble` + DROP COLUMN `defchan`; +ALTER TABLE `mumble_mumble` + DROP COLUMN `booted`; diff --git a/mumble/conversionsql/mysql/22-schema-cleanup-mumbleuser.sql b/mumble/conversionsql/mysql/22-schema-cleanup-mumbleuser.sql new file mode 100644 index 0000000..a4c57c2 --- /dev/null +++ b/mumble/conversionsql/mysql/22-schema-cleanup-mumbleuser.sql @@ -0,0 +1,3 @@ +-- Model: MumbleUser +ALTER TABLE `mumble_mumbleuser` + DROP COLUMN `isAdmin`; diff --git a/mumble/conversionsql/pgsql/01-schema-prepare-mumble.sql b/mumble/conversionsql/pgsql/01-schema-prepare-mumble.sql new file mode 100644 index 0000000..77a7f6d --- /dev/null +++ b/mumble/conversionsql/pgsql/01-schema-prepare-mumble.sql @@ -0,0 +1,13 @@ +-- Model: Mumble +BEGIN; +ALTER TABLE "mumble_mumble" + ADD "server_id" integer NULL REFERENCES "mumble_mumbleserver" ("id") DEFERRABLE INITIALLY DEFERRED; +ALTER TABLE "mumble_mumble" + ADD "display" varchar(200); +ALTER TABLE "mumble_mumble" + ALTER "port" DROP NOT NULL; + +CREATE INDEX "mumble_mumble_server_id" ON "mumble_mumble" ("server_id"); + +ALTER TABLE "mumble_mumble" DROP CONSTRAINT "mumble_mumble_addr_key"; +COMMIT; diff --git a/mumble/conversionsql/pgsql/11-data-mumble_mumbleserver.sql b/mumble/conversionsql/pgsql/11-data-mumble_mumbleserver.sql new file mode 100644 index 0000000..bf88107 --- /dev/null +++ b/mumble/conversionsql/pgsql/11-data-mumble_mumbleserver.sql @@ -0,0 +1,5 @@ +BEGIN; +INSERT INTO "mumble_mumbleserver" ( "dbus", "secret" ) +SELECT DISTINCT "dbus", '' +FROM "mumble_mumble"; +COMMIT; diff --git a/mumble/conversionsql/pgsql/12-data-mumble_mumble.sql b/mumble/conversionsql/pgsql/12-data-mumble_mumble.sql new file mode 100644 index 0000000..2d882db --- /dev/null +++ b/mumble/conversionsql/pgsql/12-data-mumble_mumble.sql @@ -0,0 +1,8 @@ +BEGIN; +UPDATE "mumble_mumble" +SET "server_id"=( + SELECT "id" + FROM "mumble_mumbleserver" + WHERE "mumble_mumbleserver"."dbus" = "mumble_mumble"."dbus" + ); +COMMIT; diff --git a/mumble/conversionsql/pgsql/21-schema-cleanup-mumble.sql b/mumble/conversionsql/pgsql/21-schema-cleanup-mumble.sql new file mode 100644 index 0000000..b6aad01 --- /dev/null +++ b/mumble/conversionsql/pgsql/21-schema-cleanup-mumble.sql @@ -0,0 +1,34 @@ +-- Model: Mumble +BEGIN; +ALTER TABLE "mumble_mumble" + DROP COLUMN "dbus"; +ALTER TABLE "mumble_mumble" + DROP COLUMN "url"; +ALTER TABLE "mumble_mumble" + DROP COLUMN "motd"; +ALTER TABLE "mumble_mumble" + DROP COLUMN "passwd"; +ALTER TABLE "mumble_mumble" + DROP COLUMN "supw"; +ALTER TABLE "mumble_mumble" + DROP COLUMN "users"; +ALTER TABLE "mumble_mumble" + DROP COLUMN "bwidth"; +ALTER TABLE "mumble_mumble" + DROP COLUMN "sslcrt"; +ALTER TABLE "mumble_mumble" + DROP COLUMN "sslkey"; +ALTER TABLE "mumble_mumble" + DROP COLUMN "obfsc"; +ALTER TABLE "mumble_mumble" + DROP COLUMN "player"; +ALTER TABLE "mumble_mumble" + DROP COLUMN "channel"; +ALTER TABLE "mumble_mumble" + DROP COLUMN "defchan"; +ALTER TABLE "mumble_mumble" + DROP COLUMN "booted"; + +ALTER TABLE "mumble_mumble" + ALTER COLUMN "server_id" SET NOT NULL; +COMMIT; \ No newline at end of file diff --git a/mumble/conversionsql/pgsql/22-schema-cleanup-mumbleuser.sql b/mumble/conversionsql/pgsql/22-schema-cleanup-mumbleuser.sql new file mode 100644 index 0000000..05b86c8 --- /dev/null +++ b/mumble/conversionsql/pgsql/22-schema-cleanup-mumbleuser.sql @@ -0,0 +1,5 @@ +-- Model: MumbleUser +BEGIN; +ALTER TABLE "mumble_mumbleuser" + DROP COLUMN "isAdmin"; +COMMIT; diff --git a/mumble/conversionsql/sqlite/01-schema-mumble.sql b/mumble/conversionsql/sqlite/01-schema-mumble.sql new file mode 100644 index 0000000..259a91f --- /dev/null +++ b/mumble/conversionsql/sqlite/01-schema-mumble.sql @@ -0,0 +1,10 @@ +CREATE TABLE "mumble_mumble_new" ( + "id" integer NOT NULL PRIMARY KEY, + "server_id" integer NOT NULL REFERENCES "mumble_mumbleserver" ("id"), + "name" varchar(200) NOT NULL, + "srvid" integer NOT NULL, + "addr" varchar(200) NOT NULL, + "port" integer, + "display" varchar(200) NOT NULL, + UNIQUE ("server_id", "srvid") +); diff --git a/mumble/conversionsql/sqlite/02-schema-mumbleuser.sql b/mumble/conversionsql/sqlite/02-schema-mumbleuser.sql new file mode 100644 index 0000000..bbf1455 --- /dev/null +++ b/mumble/conversionsql/sqlite/02-schema-mumbleuser.sql @@ -0,0 +1,10 @@ +CREATE TABLE "mumble_mumbleuser_new" ( + "id" integer NOT NULL PRIMARY KEY, + "mumbleid" integer NOT NULL, + "name" varchar(200) NOT NULL, + "password" varchar(200) NOT NULL, + "server_id" integer NOT NULL REFERENCES "mumble_mumble" ("id"), + "owner_id" integer REFERENCES "auth_user" ("id"), + UNIQUE ("server_id", "owner_id"), + UNIQUE ("server_id", "mumbleid") +); diff --git a/mumble/conversionsql/sqlite/11-data-mumbleserver.sql b/mumble/conversionsql/sqlite/11-data-mumbleserver.sql new file mode 100644 index 0000000..f53f7eb --- /dev/null +++ b/mumble/conversionsql/sqlite/11-data-mumbleserver.sql @@ -0,0 +1,3 @@ +INSERT INTO "mumble_mumbleserver" ( "dbus", "secret" ) +SELECT DISTINCT "dbus", '' +FROM "mumble_mumble"; diff --git a/mumble/conversionsql/sqlite/12-data-mumble.sql b/mumble/conversionsql/sqlite/12-data-mumble.sql new file mode 100644 index 0000000..24dcd7b --- /dev/null +++ b/mumble/conversionsql/sqlite/12-data-mumble.sql @@ -0,0 +1,11 @@ +INSERT INTO "mumble_mumble_new" +SELECT + "mumble_mumble"."id", + "mumble_mumbleserver"."id", + "mumble_mumble"."name", + "mumble_mumble"."srvid", + "mumble_mumble"."addr", + "mumble_mumble"."port", + '' +FROM "mumble_mumble" INNER JOIN "mumble_mumbleserver" +WHERE "mumble_mumbleserver"."dbus" = "mumble_mumble"."dbus"; diff --git a/mumble/conversionsql/sqlite/13-data-mumbleuser.sql b/mumble/conversionsql/sqlite/13-data-mumbleuser.sql new file mode 100644 index 0000000..f2eff3e --- /dev/null +++ b/mumble/conversionsql/sqlite/13-data-mumbleuser.sql @@ -0,0 +1,9 @@ +INSERT INTO "mumble_mumbleuser_new" +SELECT + "id", + "mumbleid", + "name", + "password", + "server_id", + "owner_id" +FROM "mumble_mumbleuser"; diff --git a/mumble/conversionsql/sqlite/21-remove-old-mumble.sql b/mumble/conversionsql/sqlite/21-remove-old-mumble.sql new file mode 100644 index 0000000..9065ed9 --- /dev/null +++ b/mumble/conversionsql/sqlite/21-remove-old-mumble.sql @@ -0,0 +1 @@ +DROP TABLE "mumble_mumble"; diff --git a/mumble/conversionsql/sqlite/22-rename-new-mumble.sql b/mumble/conversionsql/sqlite/22-rename-new-mumble.sql new file mode 100644 index 0000000..73921fb --- /dev/null +++ b/mumble/conversionsql/sqlite/22-rename-new-mumble.sql @@ -0,0 +1 @@ +ALTER TABLE "mumble_mumble_new" RENAME TO "mumble_mumble"; diff --git a/mumble/conversionsql/sqlite/23-remove-old-mumbleuser.sql b/mumble/conversionsql/sqlite/23-remove-old-mumbleuser.sql new file mode 100644 index 0000000..2ac2610 --- /dev/null +++ b/mumble/conversionsql/sqlite/23-remove-old-mumbleuser.sql @@ -0,0 +1 @@ +DROP TABLE "mumble_mumbleuser"; diff --git a/mumble/conversionsql/sqlite/24-rename-new-mumbleuser.sql b/mumble/conversionsql/sqlite/24-rename-new-mumbleuser.sql new file mode 100644 index 0000000..ebfc5d5 --- /dev/null +++ b/mumble/conversionsql/sqlite/24-rename-new-mumbleuser.sql @@ -0,0 +1 @@ +ALTER TABLE "mumble_mumbleuser_new" RENAME TO "mumble_mumbleuser"; diff --git a/mumble/forms.py b/mumble/forms.py new file mode 100644 index 0000000..96ab27d --- /dev/null +++ b/mumble/forms.py @@ -0,0 +1,324 @@ +# -*- coding: utf-8 -*- + +""" + * Copyright б╘ 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +import socket +import re + +from django import forms +from django.conf import settings +from django.forms import Form, ModelForm +from django.utils.translation import ugettext_lazy as _ + +from mumble.models import MumbleServer, Mumble, MumbleUser + + +class PropertyModelForm( ModelForm ): + """ ModelForm that gets/sets fields that are not within the model's + fields as model attributes. Necessary to get forms that manipulate + properties. + """ + + def __init__( self, *args, **kwargs ): + ModelForm.__init__( self, *args, **kwargs ); + + if self.instance: + instfields = self.instance._meta.get_all_field_names() + for fldname in self.fields: + if fldname in instfields: + continue + self.fields[fldname].initial = getattr( self.instance, fldname ) + prop = getattr( self.instance.__class__, fldname ) + if prop.__doc__: + self.fields[fldname].label = _(prop.__doc__) + + def save( self, commit=True ): + inst = ModelForm.save( self, commit=commit ) + + if commit: + self.save_to_model( inst ) + else: + # Update when the model has been saved. + from django.db.models import signals + self._update_inst = inst + signals.post_save.connect( self.save_listener, sender=inst.__class__ ) + + return inst + + def save_listener( self, **kwargs ): + if kwargs['instance'] is self._update_inst: + self.save_to_model( self._update_inst ) + + def save_to_model( self, inst ): + instfields = inst._meta.get_all_field_names() + + for fldname in self.fields: + if fldname not in instfields: + setattr( inst, fldname, self.cleaned_data[fldname] ) + + +class MumbleForm( PropertyModelForm ): + """ The Mumble Server admin form that allows to configure settings which do + not necessarily have to be reserved to the server hoster. + + Server hosters are expected to use the Django admin application instead, + where everything can be configured freely. + """ + + url = forms.CharField( required=False ) + motd = forms.CharField( required=False, widget=forms.Textarea ) + passwd = forms.CharField( required=False, help_text=_( + "Password required to join. Leave empty for public servers.") ) + supw = forms.CharField( required=False ) + obfsc = forms.BooleanField( required=False, help_text=_( + "If on, IP adresses of the clients are not logged.") ) + player = forms.CharField( required=False ) + channel = forms.CharField( required=False ) + defchan = forms.TypedChoiceField( choices=(), coerce=int, required=False ) + timeout = forms.IntegerField( required=False ) + certreq = forms.BooleanField( required=False ) + textlen = forms.IntegerField( required=False ) + html = forms.BooleanField( required=False ) + + def __init__( self, *args, **kwargs ): + PropertyModelForm.__init__( self, *args, **kwargs ) + + # Populate the `default channel' field's choices + choices = [ ('', '----------') ] + + if self.instance and self.instance.srvid is not None: + if self.instance.booted: + def add_item( item, level ): + if item.is_server or item.is_channel: + choices.append( ( item.chanid, ( "-"*level + " " + item.name ) ) ) + + self.instance.rootchan.visit(add_item) + else: + current = self.instance.defchan + if current is not None: + choices.append( ( current, "Current value: %d" % current ) ) + self.fields['defchan'].choices = choices + + class Meta: + model = Mumble; + fields = ['name']; + + +class MumbleAdminForm( MumbleForm ): + """ A Mumble Server admin form intended to be used by the server hoster. """ + + users = forms.IntegerField( required=False ) + bwidth = forms.IntegerField( required=False ) + sslcrt = forms.CharField( required=False, widget=forms.Textarea ) + sslkey = forms.CharField( required=False, widget=forms.Textarea ) + booted = forms.BooleanField( required=False ) + autoboot = forms.BooleanField( required=False ) + bonjour = forms.BooleanField( required=False ) + + class Meta: + fields = None + exclude = None + + def clean_port( self ): + """ Check if the port number is valid. """ + + port = self.cleaned_data['port']; + + if port is not None and port != '': + if port < 1 or port >= 2**16: + raise forms.ValidationError( + _("Port number %(portno)d is not within the allowed range %(minrange)d - %(maxrange)d") % { + 'portno': port, + 'minrange': 1, + 'maxrange': 2**16, + }); + return port; + return None + + +class MumbleServerForm( ModelForm ): + defaultconf = forms.CharField( label=_("Default config"), required=False, widget=forms.Textarea ) + + def __init__( self, *args, **kwargs ): + ModelForm.__init__( self, *args, **kwargs ) + + if self.instance and self.instance.id: + if self.instance.online: + confstr = "" + conf = self.instance.defaultconf + for field in conf: + confstr += "%s: %s\n" % ( field, conf[field] ) + self.fields["defaultconf"].initial = confstr + else: + self.fields["defaultconf"].initial = _("This server is currently offline.") + + class Meta: + model = MumbleServer + + +class MumbleUserForm( ModelForm ): + """ The user registration form used to register an account. """ + + password = forms.CharField( widget=forms.PasswordInput, required=False ) + + def __init__( self, *args, **kwargs ): + ModelForm.__init__( self, *args, **kwargs ); + self.server = None; + + def clean_name( self ): + """ Check if the desired name is forbidden or taken. """ + + name = self.cleaned_data['name']; + + if self.server is None: + raise AttributeError( "You need to set the form's server attribute to the server instance " + "for validation to work." ); + + if self.server.player and re.compile( self.server.player ).match( name ) is None: + raise forms.ValidationError( _( "That name is forbidden by the server." ) ); + + if not self.instance.id and len( self.server.ctl.getRegisteredPlayers( self.server.srvid, name ) ) > 0: + raise forms.ValidationError( _( "Another player already registered that name." ) ); + + return name; + + def clean_password( self ): + """ Verify a password has been given. """ + passwd = self.cleaned_data['password']; + if not passwd and ( not self.instance or self.instance.mumbleid == -1 ): + raise forms.ValidationError( _( "Cannot register player without a password!" ) ); + return passwd; + + class Meta: + model = MumbleUser; + fields = ( 'name', 'password' ); + + +class MumbleUserPasswordForm( MumbleUserForm ): + """ The user registration form used to register an account on a private server in protected mode. """ + + serverpw = forms.CharField( + label=_('Server Password'), + help_text=_('This server is private and protected mode is active. Please enter the server password.'), + widget=forms.PasswordInput(render_value=False) + ); + + def clean_serverpw( self ): + """ Validate the password """ + serverpw = self.cleaned_data['serverpw']; + if self.server.passwd != serverpw: + raise forms.ValidationError( _( "The password you entered is incorrect." ) ); + return serverpw; + + def clean( self ): + """ prevent save() from trying to store the password in the Model instance. """ + # clean() will be called after clean_serverpw(), so it has already been validated here. + if 'serverpw' in self.cleaned_data: + del( self.cleaned_data['serverpw'] ); + return self.cleaned_data; + + +class MumbleUserLinkForm( MumbleUserForm ): + """ Special registration form to either register or link an account. """ + + linkacc = forms.BooleanField( + label=_('Link account'), + help_text=_('The account already exists and belongs to me, just link it instead of creating.'), + required=False, + ); + + def __init__( self, *args, **kwargs ): + MumbleUserForm.__init__( self, *args, **kwargs ); + self.mumbleid = None; + + def clean_name( self ): + """ Check if the target account exists in Murmur. """ + if 'linkacc' not in self.data: + return MumbleUserForm.clean_name( self ); + + # Check if user exists + name = self.cleaned_data['name']; + + if len( self.server.ctl.getRegisteredPlayers( self.server.srvid, name ) ) != 1: + raise forms.ValidationError( _( "No such user found." ) ); + + return name; + + def clean_password( self ): + """ Verify that the password is correct. """ + if 'linkacc' not in self.data: + return MumbleUserForm.clean_password( self ); + + if 'name' not in self.cleaned_data: + # keep clean() from trying to find a user that CAN'T exist + self.mumbleid = -10; + return ''; + + # Validate password with Murmur + passwd = self.cleaned_data['password']; + + self.mumbleid = self.server.ctl.verifyPassword( self.server.srvid, self.cleaned_data['name'], passwd ) + if self.mumbleid <= 0: + raise forms.ValidationError( _( "The password you entered is incorrect." ) ); + + return passwd; + + def clean( self ): + """ Create the MumbleUser instance to save in. """ + if 'linkacc' not in self.data or self.mumbleid <= 0: + return self.cleaned_data; + + try: + m_user = MumbleUser.objects.get( server=self.server, mumbleid=self.mumbleid ); + except MumbleUser.DoesNotExist: + m_user = MumbleUser( server=self.server, name=self.cleaned_data['name'], mumbleid=self.mumbleid ); + m_user.save( dontConfigureMurmur=True ); + else: + if m_user.owner is not None: + raise forms.ValidationError( _( "That account belongs to someone else." ) ); + + if m_user.getAdmin() and not settings.ALLOW_ACCOUNT_LINKING_ADMINS: + raise forms.ValidationError( _( "Linking Admin accounts is not allowed." ) ); + self.instance = m_user; + + return self.cleaned_data; + + +class MumbleUserAdminForm( PropertyModelForm ): + aclAdmin = forms.BooleanField( required=False ); + password = forms.CharField( widget=forms.PasswordInput, required=False ) + + def clean_password( self ): + """ Verify a password has been given. """ + passwd = self.cleaned_data['password']; + if not passwd and ( not self.instance or self.instance.mumbleid == -1 ): + raise forms.ValidationError( _( "Cannot register player without a password!" ) ); + return passwd; + + class Meta: + model = Mumble; + + +class MumbleKickForm( Form ): + session = forms.IntegerField(); + ban = forms.BooleanField( required=False ); + reason = forms.CharField( required=False ); + + +class MumbleTextureForm( Form ): + """ The form used to upload a new image to be set as texture. """ + texturefile = forms.ImageField( label=_("User Texture") ); + + diff --git a/mumble/locale/_outdated/hr/LC_MESSAGES/django.mo b/mumble/locale/_outdated/hr/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..5f4c9f71d71ecf192f1b27f8326efb8080c73bcd GIT binary patch literal 6846 zcmai&Ta08y8OKXR(D8z3X6PoHYf zrFUnH22GS0+z=&TVtfG;eLxcvU+@JLcN2I)NlY}+7+!oaYD`ofjlZu>ot^;~In&ku zQ+4W8eV70D)z|Z<9Tz{LxRz;G(Y|+)QUkev8(&;6T%y$b-lEit;KTI)aH&%71|N94 zQZl6vUJj1H_kdpq_kd4;GVgitGVnLxJHc1L8^OPVETyh^hf-7EHK5GD5qv+mAKU>h zfHLnv@V#IUj&t>54paw(fFmOi|D61)`r12A>0Q?mw^8G$O|1&81{tFa6u0%-D;~G%p+a2*%@LKu@LE&2h$~r^v z8gLW54*V`C`~DpK9QYC_a_wRBC&5`zK#~7CP~@47xCCnY2`Kx24dkP~&lj`{*PVQeujN7=byv7k>LZ}YCj0G-V`2-r z-bP!b?WKvGg=5Wij2p3`Tw<%{nr&`C(ebv}za7*x@ymYNTWNb}!Y9#*lpQ#K6L^@m zL=#TkLld7Kp-E2KN4u3KJXxk~zh>y%8wVr~@1}|U<(jV{*hOq7{%Wp6+(7X~hTB0jhUypbmUJWiXY$<>mt zDIKl@ow;K!D`yYv?aUqM3n8V?XoqW z4~_IRciNiny6&!=zGoBtaNiXDrMP@jhn{x*{tjH z+;+=!qpd&VD}CCfsm^?<3u|?`ZgpZy<5E2`JzLakZ!oQEGLcmqW~VM2paBwyfJNM) zTsNht)u>{Vc6>6i$Y@S>%xtZ4$La${ujh8{&TFsVmHM8`9<1_ocX90;ogR~1c3`iW ztM$;PlOmu;)1E6znMGKeCgc4mgQtqxYq%}UobrA^c!40PHh z<+{HA(5U#FIwaey1j*E4oJen?N?iW5E7v{FG+4hVPWwFBsSei;Z&1%4ncN25C%wcL z!R^pxGA44#F7}K%?BPwfR7Va~g@#S@hmY!xDXcnT&6>>+xVh1t4J??hF|cN;bjvnJ z&zRvTwZ*KyA+u$>@KxTmP@req!(ezI=W|1#YqQY}ix6tI?2dG6UC-?MM0=XQ8QmJ` z+isoSSI?dAV!nGSxOE(f)iGa`L3ELGSz-ca7l8*3)|I}dZh^7Vq^6}_Z{ERO7T(&f z5x%;zX1E!RtjTMlSlvr~hX|{A0-uYrt=MR(vsM*&PfR$!#^gQq_Od&n<<~-hbW?|y zLkNe!sma<_3xq|($k1_FPY13wqlHAgIG!l-rwI_XAgrHwTP>P1ZG9543KgkbGvh&* zGy^D=>+5$7tKA5caaZJHxrNz@c1NR#bOcxMgxny5>yms^ze^IuSDj2C2ob z2VzK{u*F8!g%w2RV&JP4dzaR;rQBgI6a~ ztcJ`SQ((uYKI?^GD*VuT9B$Ks@C&m-)Zqf6y5o|>WBx;tdeOWl(U5UP}YPskVO(JkHB zw)Jvq4H+Z|w9N(+aog%7ONKAKfCcKiRePqaa$9$zrC!r1>Rzz4oS`8Fs888W;c#C> zee%Qt_h}aw=|$2Hr}3mejD0yw<-o1E5vBBwLA zV#a-Yfx>TTob)AHz&QV$%pCX}np2)4k_}<)K^Lk(dx2ta(UY$%Lh;5MReG4BZ^migW=`*_ zI7*#Ju{+d#Q6(g^92~U{j$`25*(zf|`>`KJGRFK=ZL9fCrKZT-AkH`2D?1-+z&Z_6 zWK>iHW}NxHE_}yrn~L>}S{i%ph{NEUYSWf}4~C?I=!;+$yC-?o^X+)>w&Z388^#X3GPCK%RWoH}I`hGHKbLJ8Mf83DTgo8}?D{&1*&I9!f=#=FMtF=7v&n9&d=H>?VFu z(i?dZ_&H3O_Kn`vRD-*9P*Q40ia67NgxJUs#(e_ezDR49{GWw<2-!^%jz0esUxZ8u zU=L}9*08XyC51auM>eiCZFR`u3`CmLkTHTF1#E;h@TnT4eQa(sC>1JQq-0u39>@&2 zg-#}MN)XkHm5!2%u-g=W)uB-LInmGt1t;Z&&x?UuZ}_h>$t_945!kvAC}$`-2L-X7 zLfTHCme?Z*DV%FMqH3M^D-|<;^Q?-6rev3qNt$_0q{d>E_iNV6^iZ@m#M47J$o-mB zGTWH8H5wcwp&V2oJ~DIeTQ+N}<6BGmM8#>acWbLgNlB_mSCo`VQHr)+qUH)-B(-dk z9Elk*cwk0IIT|ldg0Y~eJkkVswd!JIG8pgUO z7q#ug(Uba@Eby9O*i9bC9P#*Qs2VaiVL#zAKLqK9RNoBZtV* zGm@mvs4j*Q!r?eI=bx*>IU~XisQN;73R267!25P6M?t*; +# +# Mumble-Django is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This package is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +msgid "" +msgstr "" +"Project-Id-Version: Mumble-Django v0.8\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-03-11 16:43+0100\n" +"PO-Revision-Date: 2010-01-11 14:11\n" +"Last-Translator: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Translated-Using: django-rosetta 0.5.1\n" + +#: admin.py:34 admin.py:51 +msgid "Master is running" +msgstr "" + +#: admin.py:59 models.py:162 templates/mumble/mumble.html:28 +msgid "Server Address" +msgstr "Adresa servera" + +#: admin.py:66 models.py:165 +msgid "Server Port" +msgstr "Port" + +#: admin.py:71 +msgid "Instance is running" +msgstr "" + +#: admin.py:81 +msgid "Registered users" +msgstr "Registrirani korisnici" + +#: admin.py:91 +msgid "Online users" +msgstr "Korisnici na serveru" + +#: admin.py:101 +msgid "Channel count" +msgstr "Broj kanala" + +#: admin.py:108 +msgid "Yes" +msgstr "" + +#: admin.py:110 +msgid "No" +msgstr "" + +#: admin.py:114 +msgid "Public" +msgstr "Javni server" + +#: admin.py:132 models.py:626 templates/mumble/mumble.html:223 +msgid "Admin on root channel" +msgstr "Administrator u glavnoj sobi" + +#: forms.py:83 +msgid "Password required to join. Leave empty for public servers." +msgstr "" +"Lozinka za pristup serveru. Ostaviti prazno ako se spajate na javni server." + +#: forms.py:86 +msgid "If on, IP adresses of the clients are not logged." +msgstr "Ako je ukljuд█eno, IP adrese klijenata se neд┤e biljeе╬iti." + +#: forms.py:142 +#, python-format +msgid "" +"Port number %(portno)d is not within the allowed range %(minrange)d - %" +"(maxrange)d" +msgstr "Port %(portno)d nije u dozvoljenom opsegu %(minrange)d - %(maxrange)d" + +#: forms.py:165 templates/mumble/offline.html:12 +msgid "This server is currently offline." +msgstr "" + +#: forms.py:190 +msgid "That name is forbidden by the server." +msgstr "Ime je zabranjeno na serveru." + +#: forms.py:193 models.py:582 +msgid "Another player already registered that name." +msgstr "Ovo ime je zauzeto, probajte neko drugo." + +#: forms.py:201 forms.py:307 models.py:584 +msgid "Cannot register player without a password!" +msgstr "Odaberite lozinku i probajte ponovno!" + +#: forms.py:213 models.py:179 +msgid "Server Password" +msgstr "Lozinka servera" + +#: forms.py:214 +msgid "" +"This server is private and protected mode is active. Please enter the server " +"password." +msgstr "" +"Ovo je privatan server i potrebna vam je lozinka za pristup. Molimo vas " +"unesite lozinku." + +#: forms.py:222 forms.py:274 +msgid "The password you entered is incorrect." +msgstr "Lozinka koju ste unijeli je neispravna." + +#: forms.py:237 +msgid "Link account" +msgstr "Povezani raд█un" + +#: forms.py:238 +msgid "" +"The account already exists and belongs to me, just link it instead of " +"creating." +msgstr "Ovaj raд█un veд┤ postoji. Poveе╬ite ga umjesto da radite novi raд█un." + +#: forms.py:255 +msgid "No such user found." +msgstr "Korisnik nije pronaд▒en." + +#: forms.py:290 +msgid "That account belongs to someone else." +msgstr "Ovaj raд█un pripada drugome korisniku." + +#: forms.py:293 +msgid "Linking Admin accounts is not allowed." +msgstr "Povezivanje administratorskih raд█una nije dozvoljeno." + +#: forms.py:322 templates/mumble/mumble.html:65 +#: templates/mumble/mumble.html.py:148 templates/mumble/mumble.html:278 +msgid "User Texture" +msgstr "Korisniд█ka slika" + +#: models.py:63 +msgid "DBus or ICE base" +msgstr "DBus ili ICE" + +#: models.py:64 +msgid "" +"Examples: 'net.sourceforge.mumble.murmur' for DBus or 'Meta:tcp -h 127.0.0.1 " +"-p 6502' for Ice." +msgstr "" +"Primjeri: 'net.sourceforge.mumble.murmur' za DBus ili 'Meta:tcp -h 127.0.0.1 " +"-p 6502' za Ice." + +#: models.py:65 +msgid "Ice Secret" +msgstr "" + +#: models.py:68 +#, fuzzy +msgid "Mumble Server" +msgstr "ID korisnika na Mumbleu" + +#: models.py:69 +#, fuzzy +msgid "Mumble Servers" +msgstr "ID korisnika na Mumbleu" + +#: models.py:160 +msgid "Server Name" +msgstr "Ime servera" + +#: models.py:161 +msgid "Server ID" +msgstr "ID servera" + +#: models.py:163 +msgid "" +"Hostname or IP address to bind to. You should use a hostname here, because " +"it will appear on the global server list." +msgstr "" +"Ime posluе╬itelja (hostname) ili IP adresa adresa za spajanje. Koristite ime " +"posluе╬itelja ovdje zato е║to д┤e se pojaviti na globalnoj listi servera." + +#: models.py:166 +#, fuzzy +msgid "Port number to bind to. Leave empty to auto assign one." +msgstr "Port za spajanje. Koristite -1 za automatsko dodjeljivanje porta." + +#: models.py:167 +#, fuzzy +msgid "Server Display Address" +msgstr "Adresa servera" + +#: models.py:168 +msgid "" +"This field is only relevant if you are located behind a NAT, and names the " +"Hostname or IP address to use in the Channel Viewer and for the global " +"server list registration. If not given, the addr and port fields are used. " +"If display and bind ports are equal, you can omit it here." +msgstr "" + +#: models.py:174 +msgid "Superuser Password" +msgstr "Lozinka Superusera (administrator)" + +#: models.py:177 +msgid "Website URL" +msgstr "URL internet stranice" + +#: models.py:178 +msgid "Welcome Message" +msgstr "Poruka dobrodoе║lice" + +#: models.py:180 +msgid "Max. Users" +msgstr "Maksimalan broj korisnika" + +#: models.py:181 +msgid "Bandwidth [Bps]" +msgstr "Promet [Bps]" + +#: models.py:182 +msgid "SSL Certificate" +msgstr "SSL certifikat" + +#: models.py:183 +msgid "SSL Key" +msgstr "SSL kljuд█" + +#: models.py:184 +msgid "Player name regex" +msgstr "Dozvoljeni znakovi u nazivu korisnika" + +#: models.py:185 +msgid "Channel name regex" +msgstr "Dozvoljeni znakovi u nazivu kanala" + +#: models.py:186 +msgid "Default channel" +msgstr "Poд█etni kanal" + +#: models.py:187 +msgid "Timeout" +msgstr "" + +#: models.py:189 +msgid "IP Obfuscation" +msgstr "Biljeе╬i IP adrese korisnika" + +#: models.py:190 +#, fuzzy +msgid "Require Certificate" +msgstr "SSL certifikat" + +#: models.py:191 +msgid "Maximum length of text messages" +msgstr "" + +#: models.py:192 +msgid "Allow HTML to be used in messages" +msgstr "" + +#: models.py:193 +msgid "Publish this server via Bonjour" +msgstr "" + +#: models.py:194 +msgid "Boot Server when Murmur starts" +msgstr "" + +#: models.py:212 models.py:213 +msgid "Boot Server" +msgstr "Pokreni server" + +#: models.py:217 models.py:545 +msgid "Server instance" +msgstr "Instanca servera" + +#: models.py:218 +msgid "Server instances" +msgstr "Instance servera" + +#: models.py:505 models.py:685 +msgid "This field must not be updated once the record has been saved." +msgstr "Ovo polje ne smije biti aе╬urirano nakon е║to je zapis spremljen." + +#: models.py:542 +msgid "Mumble player_id" +msgstr "ID korisnika na Mumbleu" + +#: models.py:543 +msgid "User name and Login" +msgstr "Korisniд█ko ime" + +#: models.py:544 +msgid "Login password" +msgstr "Lozinka" + +#: models.py:546 templates/mumble/mumble.html:293 +msgid "Account owner" +msgstr "Vlasnik raд█una" + +#: models.py:548 +#, fuzzy +msgid "The user's comment." +msgstr "Korisniд█ki raд█un" + +#: models.py:549 +msgid "The user's hash." +msgstr "" + +#: models.py:553 +msgid "User account" +msgstr "Korisniд█ki raд█un" + +#: models.py:554 +msgid "User accounts" +msgstr "Korisniд█ki raд█uni" + +#: models.py:561 +#, python-format +msgid "Mumble user %(mu)s on %(srv)s owned by Django user %(du)s" +msgstr "Django korisniku %(du)s pripada Mumble raд█un %(mu)s na serveru %(srv)s" + +#: templates/mumble/list.html:20 +msgid "No server instances have been configured yet." +msgstr "" + +#: templates/mumble/mumble.html:16 +msgid "" +"\n" +" Hint:
\n" +" This area is used to display additional information for each channel " +"and player, but requires JavaScript to be\n" +" displayed correctly. You will not see the detail pages, but you can " +"use all links and forms\n" +" that are displayed.\n" +" " +msgstr "" +"\n" +" Savjet:
\n" +" Ovdje se prikazuju dodatne informacije za svaki kanali svakog igraд█a " +"te zahtjeva JavaScript kako bi\n" +" se informacije pravilno prikazale. Neд┤ete vidjeti stranicu s " +"detaljima, ali moе╬ete koristiti sve linkove i forme.\n" +" " + +#: templates/mumble/mumble.html:31 +msgid "Website" +msgstr "Internet stranica" + +#: templates/mumble/mumble.html:33 +msgid "Server version" +msgstr "Verzija servera" + +#: templates/mumble/mumble.html:34 +msgid "Minimal view" +msgstr "" + +#: templates/mumble/mumble.html:37 +msgid "Welcome message" +msgstr "Poruka dobrodoе║lice" + +#: templates/mumble/mumble.html:43 +msgid "Server registration" +msgstr "Registracija servera" + +#: templates/mumble/mumble.html:46 +msgid "You are registered on this server" +msgstr "Registrirani ste na ovom serveru" + +#: templates/mumble/mumble.html:48 +msgid "You do not have an account on this server" +msgstr "Nemate raд█un na ovom serveru" + +#: templates/mumble/mumble.html:57 +#, python-format +msgid "" +"\n" +"

You need to be logged in to be able " +"to register an account on this Mumble server.

\n" +" " +msgstr "" +"\n" +"

Morate biti prijavljeni (ulogirani) " +"kako bi ste napravili raд█un na ovom Mumble serveru.

\n" +" " + +#: templates/mumble/mumble.html:67 +msgid "" +"\n" +" Sorry, due to a bug in Murmur 1.2.2, displaying and setting the " +"Texture is disabled.\n" +" " +msgstr "" + +#: templates/mumble/mumble.html:72 +#, fuzzy +msgid "" +"\n" +" You can upload an image that you would like to use as your user " +"texture here.\n" +" " +msgstr "" +"\n" +" Moе╬ete postaviti sliku za koju bi htjeli da zamjeni vaе║ekorisniд█ko " +"ime u Mumble transparentu (overlay).\n" +" " + +#: templates/mumble/mumble.html:77 +msgid "Your current texture is" +msgstr "Vaе║a trenutaд█na slika je" + +#: templates/mumble/mumble.html:80 +msgid "You don't currently have a texture set" +msgstr "" + +#: templates/mumble/mumble.html:84 +#, fuzzy +msgid "" +"\n" +" Hint: The texture image needs to be 600x60 in size. If " +"you upload an image with\n" +" a different size, it will be resized accordingly.
\n" +" " +msgstr "" +"\n" +" Savjet: Slika mora biti veliд█ine 600x60. Ako odaberete " +"sliku drugaд█ije veliд█ine, veliд█ina д┤e biti promjenjena u 600x60.
\n" +" " + +#: templates/mumble/mumble.html:103 +msgid "Server administration" +msgstr "Administracija servera" + +#: templates/mumble/mumble.html:117 +msgid "Player" +msgstr "Korisnik" + +#: templates/mumble/mumble.html:119 +msgid "Online since" +msgstr "Na serveru od" + +#: templates/mumble/mumble.html:120 templates/mumble/player.html:9 +msgid "Authenticated" +msgstr "Registriran korisnik" + +#: templates/mumble/mumble.html:121 templates/mumble/mumble.html.py:136 +msgid "Admin" +msgstr "Administrator" + +#: templates/mumble/mumble.html:122 templates/mumble/player.html:12 +msgid "Muted" +msgstr "Utiе║an mikrofon" + +#: templates/mumble/mumble.html:123 templates/mumble/player.html:18 +msgid "Deafened" +msgstr "Utiе║ani zvuд█nici / sluе║alice" + +#: templates/mumble/mumble.html:124 templates/mumble/player.html:21 +msgid "Muted by self" +msgstr "Samo-utiе║an mikrofon" + +#: templates/mumble/mumble.html:125 templates/mumble/player.html:24 +msgid "Deafened by self" +msgstr "Samo-utiе║ani zvuд█nici / sluе║alice" + +#: templates/mumble/mumble.html:127 +#, fuzzy +msgid "IP Address" +msgstr "Adresa servera" + +#: templates/mumble/mumble.html:131 +msgid "User" +msgstr "Korisnik" + +#: templates/mumble/mumble.html:134 +msgid "Full Name" +msgstr "Ime i prezime" + +#: templates/mumble/mumble.html:137 +msgid "Sign-up date" +msgstr "Datum registracije" + +#: templates/mumble/mumble.html:142 +#, fuzzy +msgid "User Comment" +msgstr "Korisniд█ki raд█un" + +#: templates/mumble/mumble.html:154 templates/mumble/mumble.html.py:168 +msgid "Kick user" +msgstr "" + +#: templates/mumble/mumble.html:160 +msgid "Reason" +msgstr "" + +#: templates/mumble/mumble.html:165 +#, fuzzy +msgid "Ban user" +msgstr "Korisnici na serveru" + +#: templates/mumble/mumble.html:175 +msgid "Channel" +msgstr "Soba" + +#: templates/mumble/mumble.html:177 +msgid "Channel ID" +msgstr "ID kanala" + +#: templates/mumble/mumble.html:179 +msgid "Connect" +msgstr "Spoji se" + +#: templates/mumble/mumble.html:182 +msgid "Channel description" +msgstr "Opis kanala" + +#: templates/mumble/mumble.html:229 +msgid "Delete" +msgstr "" + +#: templates/mumble/mumble.html:265 +msgid "Server Info" +msgstr "Informacije o serveru" + +#: templates/mumble/mumble.html:266 +msgid "Registration" +msgstr "Registracija" + +#: templates/mumble/mumble.html:275 +msgid "Administration" +msgstr "Administracija" + +#: templates/mumble/mumble.html:282 +msgid "User List" +msgstr "Lista korisnika" + +#: templates/mumble/mumble.html:286 +msgid "name" +msgstr "" + +#: templates/mumble/mumble.html:306 +#, fuzzy +msgid "Change password" +msgstr "Lozinka" + +#: templates/mumble/mumble.html:319 +msgid "Add" +msgstr "" + +#: templates/mumble/mumble.html:331 +msgid "Save" +msgstr "" + +#: templates/mumble/mumble.html:357 +msgid "Resync with Murmur" +msgstr "Resinkroniziraj s Murmurom" + +#: templates/mumble/player.html:15 +msgid "Suppressed" +msgstr "" + +#: templates/mumble/player.html:27 +msgid "has a User Comment set" +msgstr "" + +#~ msgid "" +#~ "Enter the ID of the default channel here. The Channel viewer displays the " +#~ "ID to server admins on the channel detail page." +#~ msgstr "" +#~ "Unesite ID poд█etnog kanala. Prikaz kanala pokazuje ID administratorima " +#~ "servera na stranici s detaljima kanala." + +#~ msgid "The admin group was not found in the ACL's groups list!" +#~ msgstr "Grupa administratora nije pronaд▒ena u ACL listi grupa!" diff --git a/mumble/locale/_outdated/pl/LC_MESSAGES/django.mo b/mumble/locale/_outdated/pl/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..d3d5cb9dfb4a64492aacb9a0d790fefea43dc4a7 GIT binary patch literal 6015 zcmai%TWlRi8OI0OQtB3H8}8vUEiGxJ-+X@d#w)+3xMpeZqJ^(j%7EXwk{j3P>y*0THA;A_&0DD_+L=w-Tr!|-URLkuLWno+rcG}De9Bp2>1vn^S=yk10M%( z1iuf;yq|&BgHMC})E|8RFQBadZ}6kw$TdpwQxUfU%t4Xs&)^vNg75ESQ`vVvH(7TI z`~bKFie8U`BF|UB>%b>GJ_(M~zW`zq^#XVU_#$`$ydEWGJOE|gHBi2P2oyO#2Z}!5 z07b6HK}1qdg8bBvJg$Q>|5v{M94L1Fs~^9DMP>bMpxEmcP}bWI@>55+iT#$q_k%0o z1o$X;ANWg9Tzm^k%Kke+hSXk<_k#S?95-420Z`;lKtxbIP~`iJ$FG1l(f=m66I=)1 z2L2t~27j(bxP9PuK1vxsf$$>VMUW}#8Sow8pM3vC@HYC_VO-JcE>QHJ21U-(zP|#B zy+01(0`*x?T;Bqri25nWPd&};&ERt${|Smd*CJF4+z!h3p8(H-UjRkU zD>24B;EkZ{y8w!PPH~g(+u#`Z5XdL$yB>c6irsz#@>9R(b`boBAK!;C;-`b)VK4&4 zFW&>VgFgo4`)5G0#~=Os=RuL{KfZqrMiM)1<0j)fL79IyDEb@*-wG~*BJY~VKKO3> zp9f|Bw?SN>eh7*lKL_6j{tlc5{|6obr&&zo`ZS0L>WiT0_XH^4{lJg^!uOvA#ct1o zBG11-mQq(i7QyY{JHavVJ>W4=)@y^%N6pX<(PYl8w814Yc{fdTey<-C{)kMwX`yB7^Wra^n>3E}C4Tvs_|>N%vISMIME3a*5xB zb7Id1?RMHO+J|U9y{fnK{L1{l*Cw+@gJo_*T_ie;^0Z?rZNe}rq9isQ9mUH@)-}@8JZWo!RUNFFIJO;a z;!w*6Hru6JWuY_su`i zj{5C+Ms&DT)v5HWRfT+O#?dn4sxOlovKb z0BwRGDdR$8p+c-WS9WDqZnHBsYaZC0);3p@gL%E!W@<9*MsaX z)g+c#Wv|-QXph^%YAAy!B5KE`Yny7aWNutUfhlaL4q~8Q6c(%cfrDxO5p__uS#*-A zDV#`esES{{7Zs}s&NNs*&wEK0-lV2#hYwKCP7fcQ?!#VabLV#GG8_}RWEXo@O(pOq zDAe@9GS{$acIuFBncS*rYnE+{z=H?fs<2>ZnSo_fb~bEt=&b3c9h;Bq9kDH%c~WM9 zg#s(K*>#2oGJZQ8T?ZTOn1fK`MUd*osvg^OcXO1#G2KY@zPm>E)N^M8%y+niTgQ=D z%_Mo@L>D<1Bqm^X?syQvx+1Bmn`5j_SkqFk4!*%%7T(&x2w$V3X1GaHYqFXsR_XTDPEFRPIwlMrdQQ!m zvrT=HEpp{)S$p}xC>#tRV5V=qty}J7@tCJPJ0p*dV9$czfOQN``k6ClZYTHeV%DF2oTb(HICdr#>!6g?D zj^Lh13rPmy@wXEyr57hViPhX0Lnw?DB$1FRk{*g=C}Bm-D0YubZb;Xh^ObMuYqNj)<^J~w;E>G|V_Mvj@hXe<#xdB+qL6IG~tuPAe8*-SeVBXhHJhc-kWYmSa^6clLJSgKOw5FK}unxgoFatda-E$%tF zbhxp9W1eiYY_rB8co;_U%DCR&ii(lpx!#(PAiKQKNpzZU9Isi3TCQKHdWJNG8+z6H z`Cep;j*TP30NiaSy^8Y2*F3>mq{EKUZL@wMv@1#5#Af|msUx?HOpc?r*+q)6EY>wE zW^>(RTGgQH=|_CWPb5h*!)NgMcw}{-10d*EdEeDLor_QPG>3i?SG#n_27O{+{etDK zm;2Hqx|cJ(Xh%pX1(19ZxdO-(Jo^pXJVV4jhmnadJ-nf565E$4hvv#WVv|ZJY|hjb!ki&H8pz`EyEaa6X6v@`jseHrF|c zuZZMhF*G(^SwDwPan;>4FIY2b$uOzkGh5tH`*#foBh%WY#|qO_r#A@~R|HSrrlhN+ zm#m*h*t#N`57YV(MqZ9IXNnXuFoC><({55no#WN)bi-4?k5pdpCk9f2A zRYk3;E$fD_UJ?nN>yjws;C!{BNM^gDXu+bSH0%={vL5EYEnMU*H_eHyO? zrdw^$Rlcam7|h)T`5}IfV5-TXTbCXS`&ftiGzt1~C9bb)qS&rMZy}2-qwx$xpf+lB zO-|Su)=@{OfcpkRy8t2zMT|;HB?)pLof02Rc3Tk$p2L)RGtRZXJlZlJH*a3SsFNG7 zrC^XU2|)*CQg1V>eR(wSB{34Uvt*frw;mEt4u_`t9E*vonl|kbRD-bvNVw)XNIt;Q zY2retouk*KDUYDi^OEt6EKf#@FXl*G!U<5$fYrn$t@ zpt6!|&wr-Dn$^iL-*|QoX4h`^Z;g`bYS?lAfVuLD?fs}F*R+d@($^1Em-RQvTxT(1 zL%m;Lih?X3u4WX6tf3$nXc>9lX;*qlz0xr1R4U0cbg^G&oI01mTYp?wwd4;m;W8Ch zR3~fGE=tph`VtZH-v;ha^p-*03;K5bJfE#mc1Z>1%vW!aW}JnE5M?vfYC=Vq9>dvn zw&Gl%xY~#UCFRwY>?_eZ$y)qxvxQ7Y-EnZ)&}HA&DP_yNML7rMgplM?AByT!WWt0q zQIdC>P~@8qQ(k^_&Tym+kE5dMZusc97;*4gvYOuVdZ~BusWqMOzZ)sZIIYzG0RT|C Au>b%7 literal 0 HcmV?d00001 diff --git a/mumble/locale/_outdated/pl/LC_MESSAGES/django.po b/mumble/locale/_outdated/pl/LC_MESSAGES/django.po new file mode 100644 index 0000000..e30f447 --- /dev/null +++ b/mumble/locale/_outdated/pl/LC_MESSAGES/django.po @@ -0,0 +1,574 @@ +# Polskie tд╧Б─ umaczenia dla Mumble-Django. +# +# Copyright (C) 2009, HuNt3r +# +# Mumble-Django is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This package is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +msgid "" +msgstr "" +"Project-Id-Version: Mumble-Django v0.8\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-03-11 16:43+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: HuNt3r \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: admin.py:34 admin.py:51 +msgid "Master is running" +msgstr "" + +#: admin.py:59 models.py:162 templates/mumble/mumble.html:28 +msgid "Server Address" +msgstr "Adres serwera" + +#: admin.py:66 models.py:165 +msgid "Server Port" +msgstr "Port serwera" + +#: admin.py:71 +msgid "Instance is running" +msgstr "" + +#: admin.py:81 +msgid "Registered users" +msgstr "Zarejestrowani uе╪ytkownicy" + +#: admin.py:91 +msgid "Online users" +msgstr "Uе╪ytkownicy online" + +#: admin.py:101 +msgid "Channel count" +msgstr "Kanaе┌y" + +#: admin.py:108 +msgid "Yes" +msgstr "" + +#: admin.py:110 +msgid "No" +msgstr "" + +#: admin.py:114 +msgid "Public" +msgstr "Publiczny" + +#: admin.py:132 models.py:626 templates/mumble/mumble.html:223 +msgid "Admin on root channel" +msgstr "Admin na kanale gе┌цЁwnym" + +#: forms.py:83 +msgid "Password required to join. Leave empty for public servers." +msgstr "Podaj hasе┌o, lub pozostaw puste pole, aby serwer byе┌ publiczny. " + +#: forms.py:86 +msgid "If on, IP adresses of the clients are not logged." +msgstr "Gdy zaznaczysz, adresy IP uе╪ytkownikцЁw nie bд≥dд┘ logowane." + +#: forms.py:142 +#, python-format +msgid "" +"Port number %(portno)d is not within the allowed range %(minrange)d - %" +"(maxrange)d" +msgstr "Numer portu %(portno)d jest z poza zakresu %(minrange)d - %(maxrange)d" + +#: forms.py:165 templates/mumble/offline.html:12 +msgid "This server is currently offline." +msgstr "" + +#: forms.py:190 +msgid "That name is forbidden by the server." +msgstr "" + +#: forms.py:193 models.py:582 +msgid "Another player already registered that name." +msgstr "Ta nazwa uе╪ytkownika jest juе╪ zajд≥ta." + +#: forms.py:201 forms.py:307 models.py:584 +msgid "Cannot register player without a password!" +msgstr "Musisz podaд┤ hasе┌o!" + +#: forms.py:213 models.py:179 +msgid "Server Password" +msgstr "Hasе┌o serwera" + +#: forms.py:214 +msgid "" +"This server is private and protected mode is active. Please enter the server " +"password." +msgstr "Ten serwer jest prywatny. Podaj hasе┌o aby siд≥ poе┌д┘czyд┤." + +#: forms.py:222 forms.py:274 +msgid "The password you entered is incorrect." +msgstr "Wprowadzone hasе┌o jest niepoprawne" + +#: forms.py:237 +msgid "Link account" +msgstr "" + +#: forms.py:238 +msgid "" +"The account already exists and belongs to me, just link it instead of " +"creating." +msgstr "" + +#: forms.py:255 +msgid "No such user found." +msgstr "" + +#: forms.py:290 +msgid "That account belongs to someone else." +msgstr "" + +#: forms.py:293 +msgid "Linking Admin accounts is not allowed." +msgstr "" + +#: forms.py:322 templates/mumble/mumble.html:65 +#: templates/mumble/mumble.html.py:148 templates/mumble/mumble.html:278 +msgid "User Texture" +msgstr "Awatar uе╪ytkownika" + +#: models.py:63 +msgid "DBus or ICE base" +msgstr "DBus lub baza ICE" + +#: models.py:64 +msgid "" +"Examples: 'net.sourceforge.mumble.murmur' for DBus or 'Meta:tcp -h 127.0.0.1 " +"-p 6502' for Ice." +msgstr "" +"Przykе┌д┘d: 'net.sourceforge.mumble.murmur' dla DBus oder 'Meta:tcp -h " +"127.0.0.1 -p 6502' dla Ice." + +#: models.py:65 +msgid "Ice Secret" +msgstr "" + +#: models.py:68 +#, fuzzy +msgid "Mumble Server" +msgstr "ID uе╪ytkownika" + +#: models.py:69 +#, fuzzy +msgid "Mumble Servers" +msgstr "ID uе╪ytkownika" + +#: models.py:160 +msgid "Server Name" +msgstr "Nazwa serwera" + +#: models.py:161 +msgid "Server ID" +msgstr "Serwer-ID" + +#: models.py:163 +msgid "" +"Hostname or IP address to bind to. You should use a hostname here, because " +"it will appear on the global server list." +msgstr "" +"Nazwa HOSTa lub adres IP. Zalecamy uе╪ywaд┤ nazwy HOSTa, poniewaе╪serwer bд≥dzie " +"widoczny na globalnej liе⌡cie serwerцЁw" + +#: models.py:166 +#, fuzzy +msgid "Port number to bind to. Leave empty to auto assign one." +msgstr "Numer portu. Wpisz -1 aby uе╪yд┤ wonego portu." + +#: models.py:167 +#, fuzzy +msgid "Server Display Address" +msgstr "Adres serwera" + +#: models.py:168 +msgid "" +"This field is only relevant if you are located behind a NAT, and names the " +"Hostname or IP address to use in the Channel Viewer and for the global " +"server list registration. If not given, the addr and port fields are used. " +"If display and bind ports are equal, you can omit it here." +msgstr "" + +#: models.py:174 +msgid "Superuser Password" +msgstr "Hasе┌o SuperUser-a" + +#: models.py:177 +msgid "Website URL" +msgstr "Adres strony" + +#: models.py:178 +msgid "Welcome Message" +msgstr "Wiadomoе⌡д┤ powitalna" + +#: models.py:180 +msgid "Max. Users" +msgstr "Max. Uе╪ytkownikцЁw" + +#: models.py:181 +msgid "Bandwidth [Bps]" +msgstr "Przepustowoе⌡д┤ [Bps]" + +#: models.py:182 +msgid "SSL Certificate" +msgstr "SSL-Certyfikat" + +#: models.py:183 +msgid "SSL Key" +msgstr "SSL-Klucz" + +#: models.py:184 +msgid "Player name regex" +msgstr "Zabronione znaki w nazwie uе╪ytkownika" + +#: models.py:185 +msgid "Channel name regex" +msgstr "Zabronione znaki w nazwie kanaе┌u" + +#: models.py:186 +msgid "Default channel" +msgstr "Gе┌цЁwny kanaе┌" + +#: models.py:187 +msgid "Timeout" +msgstr "" + +#: models.py:189 +msgid "IP Obfuscation" +msgstr "IP-maskowanie" + +#: models.py:190 +#, fuzzy +msgid "Require Certificate" +msgstr "SSL-Certyfikat" + +#: models.py:191 +msgid "Maximum length of text messages" +msgstr "" + +#: models.py:192 +msgid "Allow HTML to be used in messages" +msgstr "" + +#: models.py:193 +msgid "Publish this server via Bonjour" +msgstr "" + +#: models.py:194 +msgid "Boot Server when Murmur starts" +msgstr "" + +#: models.py:212 models.py:213 +msgid "Boot Server" +msgstr "Odpal serwer" + +#: models.py:217 models.py:545 +msgid "Server instance" +msgstr "Aktywne serwery" + +#: models.py:218 +msgid "Server instances" +msgstr "Aktywne serwery" + +#: models.py:505 models.py:685 +msgid "This field must not be updated once the record has been saved." +msgstr "To pole nie musi byд┤ aktualizowane, byе┌o aktualizowane wczeе⌡niej." + +#: models.py:542 +msgid "Mumble player_id" +msgstr "ID uе╪ytkownika" + +#: models.py:543 +msgid "User name and Login" +msgstr "Nazwa uе╪ytkownika" + +#: models.py:544 +msgid "Login password" +msgstr "Hasе┌o" + +#: models.py:546 templates/mumble/mumble.html:293 +msgid "Account owner" +msgstr "Wе┌aе⌡ciciel konta" + +#: models.py:548 +#, fuzzy +msgid "The user's comment." +msgstr "Konto uе╪ytkownika" + +#: models.py:549 +msgid "The user's hash." +msgstr "" + +#: models.py:553 +msgid "User account" +msgstr "Konto uе╪ytkownika" + +#: models.py:554 +msgid "User accounts" +msgstr "Konta uе╪ytkownikцЁw" + +#: models.py:561 +#, python-format +msgid "Mumble user %(mu)s on %(srv)s owned by Django user %(du)s" +msgstr "Uе╪ytkownik %(mu)s na %(srv)s wе┌aе⌡ciciel %(du)s" + +#: templates/mumble/list.html:20 +msgid "No server instances have been configured yet." +msgstr "" + +#: templates/mumble/mumble.html:16 +msgid "" +"\n" +" Hint:
\n" +" This area is used to display additional information for each channel " +"and player, but requires JavaScript to be\n" +" displayed correctly. You will not see the detail pages, but you can " +"use all links and forms\n" +" that are displayed.\n" +" " +msgstr "" +"\n" +" Info
\n" +" To pole jest uе╪ywane do wyе⌡wietlenia dodatkowych informacji dla " +"kaе╪degokanaе┌u i uе╪ytkownika, ale wymaga JavaScript aby\n" +" dziaе┌aе┌o poprawnie. Nie zobaczysz detalцЁw strony, lecz moе╪esz uе╪ywaд┤ " +"wszystkich linkцЁw i formatцЁw\n" +" ktцЁre zostanд┘ wyе⌡wietlone.\n" +" " + +#: templates/mumble/mumble.html:31 +msgid "Website" +msgstr "Strona" + +#: templates/mumble/mumble.html:33 +msgid "Server version" +msgstr "Wersja serwera" + +#: templates/mumble/mumble.html:34 +msgid "Minimal view" +msgstr "" + +#: templates/mumble/mumble.html:37 +msgid "Welcome message" +msgstr "Wiadomoе⌡д┤ powitalna" + +#: templates/mumble/mumble.html:43 +msgid "Server registration" +msgstr "Rejestracja konta na serwerze" + +#: templates/mumble/mumble.html:46 +msgid "You are registered on this server" +msgstr "Masz konto na tym serwerze" + +#: templates/mumble/mumble.html:48 +msgid "You do not have an account on this server" +msgstr "Nie masz konta na tym serwerze" + +#: templates/mumble/mumble.html:57 +#, python-format +msgid "" +"\n" +"

You need to be logged in to be able " +"to register an account on this Mumble server.

\n" +" " +msgstr "" +"\n" +"

Musisz byд┤ zalogowany aby mцЁc " +"zarejestrowaд┤ konto na serwerze.

\n" +" " + +#: templates/mumble/mumble.html:67 +msgid "" +"\n" +" Sorry, due to a bug in Murmur 1.2.2, displaying and setting the " +"Texture is disabled.\n" +" " +msgstr "" + +#: templates/mumble/mumble.html:72 +#, fuzzy +msgid "" +"\n" +" You can upload an image that you would like to use as your user " +"texture here.\n" +" " +msgstr "" +"\n" +" Moе╪esz wysе┌aд┤ awatar jeе⌡li chcesz.\n" +" " + +#: templates/mumble/mumble.html:77 +msgid "Your current texture is" +msgstr "TwцЁj obecny awatar" + +#: templates/mumble/mumble.html:80 +msgid "You don't currently have a texture set" +msgstr "" + +#: templates/mumble/mumble.html:84 +#, fuzzy +msgid "" +"\n" +" Hint: The texture image needs to be 600x60 in size. If " +"you upload an image with\n" +" a different size, it will be resized accordingly.
\n" +" " +msgstr "" +"\n" +" Dozwolony rozmiar pliku to: 600x60 pikseli. Kaе╪dy inny " +"rozmiar zostanie automatycznie zmieniony na podany wyд╧д╫ej. " + +#: templates/mumble/mumble.html:103 +msgid "Server administration" +msgstr "Administracja serwerem" + +#: templates/mumble/mumble.html:117 +msgid "Player" +msgstr "Uе╪ytkownik" + +#: templates/mumble/mumble.html:119 +msgid "Online since" +msgstr "Czas online" + +#: templates/mumble/mumble.html:120 templates/mumble/player.html:9 +msgid "Authenticated" +msgstr "Zarejestrowany" + +#: templates/mumble/mumble.html:121 templates/mumble/mumble.html.py:136 +msgid "Admin" +msgstr "Administrator" + +#: templates/mumble/mumble.html:122 templates/mumble/player.html:12 +msgid "Muted" +msgstr "Wyciszony mikrofon" + +#: templates/mumble/mumble.html:123 templates/mumble/player.html:18 +msgid "Deafened" +msgstr "Wyciszone sе┌uchawki" + +#: templates/mumble/mumble.html:124 templates/mumble/player.html:21 +msgid "Muted by self" +msgstr "Wyciszyе┌ mikrofon" + +#: templates/mumble/mumble.html:125 templates/mumble/player.html:24 +msgid "Deafened by self" +msgstr "Wyciszyе┌ sе┌uchawki" + +#: templates/mumble/mumble.html:127 +#, fuzzy +msgid "IP Address" +msgstr "Adres serwera" + +#: templates/mumble/mumble.html:131 +msgid "User" +msgstr "Uе╪ytkownik" + +#: templates/mumble/mumble.html:134 +msgid "Full Name" +msgstr "Peе┌na nazwa" + +#: templates/mumble/mumble.html:137 +msgid "Sign-up date" +msgstr "Data rejestracji" + +#: templates/mumble/mumble.html:142 +#, fuzzy +msgid "User Comment" +msgstr "Konto uе╪ytkownika" + +#: templates/mumble/mumble.html:154 templates/mumble/mumble.html.py:168 +msgid "Kick user" +msgstr "" + +#: templates/mumble/mumble.html:160 +msgid "Reason" +msgstr "" + +#: templates/mumble/mumble.html:165 +#, fuzzy +msgid "Ban user" +msgstr "Uе╪ytkownicy online" + +#: templates/mumble/mumble.html:175 +msgid "Channel" +msgstr "Kanaе┌" + +#: templates/mumble/mumble.html:177 +msgid "Channel ID" +msgstr "ID-kanaе┌u" + +#: templates/mumble/mumble.html:179 +msgid "Connect" +msgstr "Poе┌д┘cz" + +#: templates/mumble/mumble.html:182 +msgid "Channel description" +msgstr "Opis kanaе┌u" + +#: templates/mumble/mumble.html:229 +msgid "Delete" +msgstr "" + +#: templates/mumble/mumble.html:265 +msgid "Server Info" +msgstr "Informacje o serwerze" + +#: templates/mumble/mumble.html:266 +msgid "Registration" +msgstr "Rejestracja" + +#: templates/mumble/mumble.html:275 +msgid "Administration" +msgstr "Administracja" + +#: templates/mumble/mumble.html:282 +msgid "User List" +msgstr "Lista uе╪ytkownikцЁw" + +#: templates/mumble/mumble.html:286 +msgid "name" +msgstr "" + +#: templates/mumble/mumble.html:306 +#, fuzzy +msgid "Change password" +msgstr "Hasе┌o" + +#: templates/mumble/mumble.html:319 +msgid "Add" +msgstr "" + +#: templates/mumble/mumble.html:331 +msgid "Save" +msgstr "" + +#: templates/mumble/mumble.html:357 +msgid "Resync with Murmur" +msgstr "" + +#: templates/mumble/player.html:15 +msgid "Suppressed" +msgstr "" + +#: templates/mumble/player.html:27 +msgid "has a User Comment set" +msgstr "" + +#~ msgid "" +#~ "Enter the ID of the default channel here. The Channel viewer displays the " +#~ "ID to server admins on the channel detail page." +#~ msgstr "" +#~ "Podaj numer ID gе┌цЁwnego kanaе┌u. Numer ID moе╪na sprawdziд┤ w Channel-Viewer " + +#~ msgid "The admin group was not found in the ACL's groups list!" +#~ msgstr "Grupa admin nie istnieje na liе⌡cie grup ACL" diff --git a/mumble/locale/de/LC_MESSAGES/django.mo b/mumble/locale/de/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..f24b3d22f224b3a69d730f75fa8b49b1e6a7a602 GIT binary patch literal 9916 zcma)>dyE~|UB@RSF0fGAq?ABP+mleoNxb)F?KBS@*Z8qcvhl8MdDk&*1L@p5XYU>F z+?kts?0T0LwdEx>{J}$nidK|V1|g^rslW}2CH~l|MIr)(cohDCBGiIJCH{z1R1lBP z_nb5L-gOedkNwV>Igj7_arW@e|-7@FJ-5e+JaL zPl7CoejU6E{2Zute*@kE{xhic{|>$j+{Z`F-w*OCIt*(5aej#2bKnnx9|R@eUja4m z*T6S`zXNu_7eLYR_n`KBC7_r+!uU;~_bQ0F`hO5V#LQ=_Lr$-Mw?2Dd;+h&}{<5BMx7`FsXMHPL4Sz6fgl7sB{2K-te% z!uxN4{EOZQ(K`1wQ2QPPMb8mXbUX%1uI~r;gEru^;5o)GfYQfJOx8YkgWC5fcpG-T z0v=;*`6zq63O)&b5foqV_-@bFeo*J$3yPnI!uUkMv!L|z0Z`|ppw@pF6yHAuir=3P z_&g~8c>&b9zYpF6{x#_6MHsg+z8jPrkAsr$CGZ~TehR$G_^0`J2|UQ;kAgn|9_0JJ z?~S65Gd}cv{@m-J=y(|vonHnyD*75Ix&8~`#uxK9|ac}KL=|6FNODi z21-s}1;ytpp!mLxAf@+r1$+;v^G<>ff=`9_zX|4RY? z3Y7f63Tpnpg1AETpP=5qp9N)Kp9Hn;bD-?}B~bc%Ip9BlZ(#flkbluP`Ed|@ z7ffh;1~lL^Af!aU0NxCK9*pM9oO`?$9B!qy4fxo(~PqsPE4GKm0M{&yy1}8B#}53h%D?Wo!+nk+F{x&dQ_ zX)0^tN*_u+mlI}(5Y?SIXZDoUqAa)PP13k6V$*8|Vt%D5M{IMbd$@a;uW2>T;%&%A z0*R??UF*3ZK^V{E1Jd&?G_N+fGko2B^%93T`CScMauf?%sMC%552)ND}^!(%k>l>asurLAL%aXOvF% z(TOZ8w#?&eE6agur!(wjWUES2icTOu?9F(EAS0BUY)>c^yRzOq9@%bmqG4`cr~SCL zNpw=C4!wFvw$h{?nh%^DS3eS+6h*6!k=bXn#p2ddt2!?0DmsNpFnJ=FK!)74z%*iX zSyfxO;2qH^%!e4zzHnpAsYSE2IC*rEos1G&IitcDC-20MPPJE}Qw3JpucO72O=Ymv zrBkO(FRpB~XybL8!)W_pdfVtHTj#}k++1&WrL7~Pagon^qBIpJFV>x__OldA za>2yN0=(O>ZH{H+?0`#@O`a3CqBCiK!&}-i?%R4!q=CYk^N03rrG?rMZ!flRa0_ABv~uDJ+vRm5Y8ZcJwL0p=`PiI-DvSxIv;QD!z%yA=hG4TipF#7^Fw zy*)pjChZG3jM;zpsJRDGHY%(}ItO6Tv$D$U&^lRFY!d?gH{*)3HLwAMaLBk$uzKdLGnM2c znvKoLBEO8YMi*>cVb2#r)JBwEI{rlnu@}6A%qd&e>ADMztf;p0KKTh7@|ihWU0vQY zMdLGeJ6c7WkvE}G3uO|Zl8Ay8l;0_fk{WM9rQKi+B#3hV4 z)Hm+at7EtPabf|kVphByq??vdt=*JB+#YgL&xQ}woS3);laddro!4ompMyKZ*}M~= z3a?@$dDq6(w6k&0czbK%}Bj*#&Dj&@%yo7+|#On1gx%L~%!JKjs zMXoE9y=um|OgGutGgB5dd7fiNMdI>H+^=D}YtCmlI>sk`yW_(V;}8YeHpM=hI=aXg z_rsj>y7z-+#Oa?xdF=88Hds> zdY`Q#)ub{cuTf{MZ9VCEa5Iv^3ECxx*Qh-A%*+o^)=cE;mrNQH8VPM!wlgLl(kha! zVj}^yvcXg9NQ_43%i=Pk?<^&qCwa0|QH0hjd232RR zIAZ3`pIhska&B7Woki4n#2j8Yw9r|&zjNq4=Fr1O4&Q%}@d6V&7wl%Lp!C}F9++Du z`*+sJA5|9DMR~*=P3-Z+b`yJSZe?lZ^pw6s-Gw$xAE9BGrzMe;2l5|`*ZWufjYHVfScy7$jT zbM#$XHl=10sXcu722yJ%ev9rX)410 zkRml5H2Hu?*sKpI0-=pfD)XovW|)tHp3kbj-z5~(bZ%{L8|T`ZJW*`%PINpjRz;Rs z-@RUHD&8Ip5dz~4!T@{_$U{Hq`(LMrSGB;4T3y0XX;Z}HL^p8k)rQJ(r>SD-&okD7KvKjGIv)vgC+5RfpdS zbBC7L$+K(F5R@U8oh{0E31`vu#+4<^Kl;k2ZP!*#SlT~IFfq3GDdAQSoKAi)Sa8Q^9|_= z4e=5lH+HX)?6Afgd*mLMHnOq^C)#PyHF}9$ci_`qq5hfjhdUv2sVR4Ch84%z>=e~A zr^_sE$V#qlX7<^*KXk%ox6_OX+mKRD^ro6dA~Uq}ymsa_a-A79i_C<~IOPE=Yuo7c zbVp_vwcbJ7DaKqs-P1Z78tNbx*E92d$<_NiCX7)!ULQQdzpJA&Y&DUfS9dYQ6GK(j zw%nzY>K$#tUz^>l;x5;EB;$T|mL{Eb1+91}wDoXMe5W#QN)=sDFYWy+*tQ~n9^qyx z5O`99+cBhG*J5{8>k_Mo)(|xzrnXt88a3--M)jU|w1Y`pCPZA7IMHetcTj~}1|jUe zyn7A8csNP)bPo0ZXuA~+eBUjq{?E=^(_QQRB;l&6B`N|<=~O+_ggXw;cTTwGnBvA; zg)D>rt%Xy$U@|xvUWiS(04T53LybIK$jhhIAH_CmMve-t-o_LVRbUeF+1S^4c!)ni zO*$n_DWPzis(!ttXy#GIT;$s$a;CgXt$U}n5DE?@6UA{n28u$go^Iq1J{3B7Z)7M% zHH}XjSeMcgdR1>FeG!}uS*KZ_vHA_#d(IB-UKyrt`Xr4{WD)3RsLA( zd=@q*)cZ5XWwE2XkZ`7J^;>uI-F{5KTSk>+j7@4MrQCeR;X}tyURO|zLe=6ozhShr z=o})1^O-a?aqv(Ow*9~2UwZ+dM2&B`_|-Q4t93IP-RKy?;OU zO;UToAicCJW>T-aCz!R#Fek(WClOyFPBW$B+pWRp-Ax09MpD9 z1J^}G+ywddH@MdI^;%m@4D4|C8fDdGJ5u}QbNQ&-PCHA|@-^LNF*2URU|f?BbHk4A z&OyqXBzqNg?Kz{g?8;L@M02Is8P`^D7lv;TSn9ZT;hx_vH24fL4tA9gWmbzAA+|Ok zz2apv`CAcroeH}@oOo*IjBiTg(%s1*EajZaObw^&pZ)Lfx3=!9De?a~(HX6ghewasZ> z{yvq#J@w}#3;Be(1P89(n(HfGN>bYvbpW3>;jAME~a?&>m{7NW1%$Q{R%BgcQ!FPm{A$h7U z_KapigQAWjdU|Q1B01u=#0gNhC+@{1ev7-|e4(1;^5`{RJ`qP0T|J3#rW*-@orWU9 z$m`DX0>5D{w;{xOrxB_rpObwzFMV2!jo%U}QMYs2?zL@Z`Tdjn3eBOFf0Z;RyC`5q F{{sLZv<3hG literal 0 HcmV?d00001 diff --git a/mumble/locale/de/LC_MESSAGES/django.po b/mumble/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..73adecf --- /dev/null +++ b/mumble/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,530 @@ +# German translation file for Mumble-Django. +# +# Copyright б╘ 2009-2010, Michael "Svedrin" Ziegler +# +# Mumble-Django is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This package is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +msgid "" +msgstr "" +"Project-Id-Version: Mumble-Django v0.8\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-03-14 18:23+0100\n" +"PO-Revision-Date: 2010-03-14 18:25\n" +"Last-Translator: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Translated-Using: django-rosetta 0.5.3\n" + +#: admin.py:34 admin.py:51 +msgid "Master is running" +msgstr "Serverprozess lц╓uft" + +#: admin.py:59 models.py:162 templates/mumble/mumble.html:28 +msgid "Server Address" +msgstr "Serveradresse" + +#: admin.py:66 models.py:165 +msgid "Server Port" +msgstr "Serverport" + +#: admin.py:71 +msgid "Instance is running" +msgstr "Serverinstanz lц╓uft" + +#: admin.py:81 +msgid "Registered users" +msgstr "Registrierte Benutzer" + +#: admin.py:91 +msgid "Online users" +msgstr "Benutzer online" + +#: admin.py:101 +msgid "Channel count" +msgstr "Channels" + +#: admin.py:108 +msgid "Yes" +msgstr "Ja" + +#: admin.py:110 +msgid "No" +msgstr "Nein" + +#: admin.py:114 +msgid "Public" +msgstr "ц√ffentlich" + +#: admin.py:132 models.py:631 templates/mumble/mumble.html:223 +msgid "Admin on root channel" +msgstr "Admin im Wurzelkanal" + +#: forms.py:83 +msgid "Password required to join. Leave empty for public servers." +msgstr "Passwort was zum Verbinden benц╤tigt wird. Lasse es leer, wenn der Server ц╤ffentlich sein soll." + +#: forms.py:86 +msgid "If on, IP adresses of the clients are not logged." +msgstr "Wenn das an ist werden IP-Adressen der Clients nicht ins Log geschrieben." + +#: forms.py:142 +#, python-format +msgid "Port number %(portno)d is not within the allowed range %(minrange)d - %(maxrange)d" +msgstr "Portnummer %(portno)d liegt nicht in %(minrange)d - %(maxrange)d" + +#: forms.py:152 +msgid "Default config" +msgstr "Standard-Einstellungen" + +#: forms.py:165 templates/mumble/offline.html:12 +msgid "This server is currently offline." +msgstr "Dieser Server ist im Moment nicht erreichbar." + +#: forms.py:190 +msgid "That name is forbidden by the server." +msgstr "Dieser Name wird vom Server nicht erlaubt." + +#: forms.py:193 models.py:587 +msgid "Another player already registered that name." +msgstr "Ein anderer Spieler hat sich unter diesem Namen bereits registriert." + +#: forms.py:201 forms.py:307 models.py:589 +msgid "Cannot register player without a password!" +msgstr "Kann Account nicht ohne Passwort registrieren!" + +#: forms.py:213 models.py:179 +msgid "Server Password" +msgstr "Serverpasswort" + +#: forms.py:214 +msgid "This server is private and protected mode is active. Please enter the server password." +msgstr "Dieser Server ist privat und der Registrierungsschutz ist aktiv. Bitte gib das Serverpasswort ein." + +#: forms.py:222 forms.py:274 +msgid "The password you entered is incorrect." +msgstr "Das eingegebene Passwort ist falsch" + +#: forms.py:237 +msgid "Link account" +msgstr "Accounts verknц╪pfen" + +#: forms.py:238 +msgid "The account already exists and belongs to me, just link it instead of creating." +msgstr "Dieser Account existiert bereits und gehц╤rt mir. Verknц╪pfe die Konten nur, anstatt ein neues zu erstellen." + +#: forms.py:255 +msgid "No such user found." +msgstr "Benutzer nicht gefunden." + +#: forms.py:290 +msgid "That account belongs to someone else." +msgstr "Dieser Account gehц╤rt jemand anderem." + +#: forms.py:293 +msgid "Linking Admin accounts is not allowed." +msgstr "Admin-Accounts zu verknц╪pfen ist nicht erlaubt." + +#: forms.py:322 templates/mumble/mumble.html:65 +#: templates/mumble/mumble.html.py:148 templates/mumble/mumble.html:278 +msgid "User Texture" +msgstr "Benutzertextur" + +#: models.py:63 +msgid "DBus or ICE base" +msgstr "DBus- oder ICE-String" + +#: models.py:64 +msgid "Examples: 'net.sourceforge.mumble.murmur' for DBus or 'Meta:tcp -h 127.0.0.1 -p 6502' for Ice." +msgstr "Beispiele: 'net.sourceforge.mumble.murmur' fц╪r DBus oder 'Meta:tcp -h 127.0.0.1 -p 6502' fц╪r Ice." + +#: models.py:65 +msgid "Ice Secret" +msgstr "Ice-Passwort" + +#: models.py:68 models.py:159 +msgid "Mumble Server" +msgstr "Mumble-Server" + +#: models.py:69 +msgid "Mumble Servers" +msgstr "Mumble-Server" + +#: models.py:160 +msgid "Server Name" +msgstr "Servername" + +#: models.py:161 +msgid "Server ID" +msgstr "Server-ID" + +#: models.py:163 +msgid "Hostname or IP address to bind to. You should use a hostname here, because it will appear on the global server list." +msgstr "Hostname oder IP-Adresse unter der der Server erreichbar sein soll. Du solltest einen Hostname verwenden, da dieses Feld in der globalen Serverliste erscheint." + +#: models.py:166 +msgid "Port number to bind to. Leave empty to auto assign one." +msgstr "Portnummer auf die gebunden werden soll. Lasse das Feld leer um automatisch eine zuzuweisen." + +#: models.py:167 +msgid "Server Display Address" +msgstr "Angezeigte Adresse" + +#: models.py:168 +msgid "This field is only relevant if you are located behind a NAT, and names the Hostname or IP address to use in the Channel Viewer and for the global server list registration. If not given, the addr and port fields are used. If display and bind ports are equal, you can omit it here." +msgstr "Dieses Feld ist nur relevant wenn der Server hinter einem NAT steht, und gibt dann den Hostnamen oder die IP-Adresse die im Channel-Viewer und fц╪r die Registrierung in der Serverliste benutzt werden soll. Ist dieses Feld leer, werden die Angaben Addresse und Port stattdessen benutzt. Wenn die Ports identisch sind, kannst du den Port hier weglassen." + +#: models.py:174 +msgid "Superuser Password" +msgstr "SuperUser-Passwort" + +#: models.py:177 +msgid "Website URL" +msgstr "URL der Webseite" + +#: models.py:178 +msgid "Welcome Message" +msgstr "Willkommensnachricht" + +#: models.py:180 +msgid "Max. Users" +msgstr "Max. Benutzer" + +#: models.py:181 +msgid "Bandwidth [Bps]" +msgstr "Bandbreite [Bps]" + +#: models.py:182 +msgid "SSL Certificate" +msgstr "SSL-Zertifikat" + +#: models.py:183 +msgid "SSL Key" +msgstr "SSL-Schlц╪ssel" + +#: models.py:184 +msgid "Player name regex" +msgstr "Regex fц╪r Spielernamen" + +#: models.py:185 +msgid "Channel name regex" +msgstr "Regex fц╪r Channelnamen" + +#: models.py:186 +msgid "Default channel" +msgstr "Standardchannel" + +#: models.py:187 +msgid "Timeout" +msgstr "Timeout" + +#: models.py:189 +msgid "IP Obfuscation" +msgstr "IP-Adressen anonymisieren" + +#: models.py:190 +msgid "Require Certificate" +msgstr "SSL-Zertifikat erzwingen" + +#: models.py:191 +msgid "Maximum length of text messages" +msgstr "Maximale Lц╓nge von Textnachrichten" + +#: models.py:192 +msgid "Allow HTML to be used in messages" +msgstr "Erlaube HTML in Nachrichten" + +#: models.py:193 +msgid "Publish this server via Bonjour" +msgstr "Server via Bonjour bekannt machen" + +#: models.py:194 +msgid "Boot Server when Murmur starts" +msgstr "Instanz starten wenn Murmur startet" + +#: models.py:212 models.py:213 +msgid "Boot Server" +msgstr "Server starten" + +#: models.py:217 models.py:550 +msgid "Server instance" +msgstr "Serverinstanz" + +#: models.py:218 +msgid "Server instances" +msgstr "Serverinstanzen" + +#: models.py:510 models.py:690 +msgid "This field must not be updated once the record has been saved." +msgstr "Dieses Feld darf nicht mehr verц╓ndert werden, nachdem der Eintrag zum ersten Mal gespeichert wurde." + +#: models.py:547 +msgid "Mumble player_id" +msgstr "ID des Spielers in Murmur" + +#: models.py:548 +msgid "User name and Login" +msgstr "Benutzername und Login" + +#: models.py:549 +msgid "Login password" +msgstr "Passwort" + +#: models.py:551 templates/mumble/mumble.html:293 +msgid "Account owner" +msgstr "Accountbesitzer" + +#: models.py:553 +msgid "The user's comment." +msgstr "Benutzer-Kommentar" + +#: models.py:554 +msgid "The user's hash." +msgstr "Signatur des Zertifikats" + +#: models.py:558 +msgid "User account" +msgstr "Benutzerkonto" + +#: models.py:559 +msgid "User accounts" +msgstr "Benutzerkonten" + +#: models.py:566 +#, python-format +msgid "Mumble user %(mu)s on %(srv)s owned by Django user %(du)s" +msgstr "Benutzeraccount %(mu)s auf %(srv)s mit Besitzer %(du)s" + +#: templates/mumble/list.html:20 +msgid "No server instances have been configured yet." +msgstr "Es sind noch keine Serverinstanzen konfiguriert." + +#: templates/mumble/mumble.html:16 +msgid "" +"\n" +" Hint:
\n" +" This area is used to display additional information for each channel and player, but requires JavaScript to be\n" +" displayed correctly. You will not see the detail pages, but you can use all links and forms\n" +" that are displayed.\n" +" " +msgstr "" +"\n" +" Hinweis:
\n" +" Dieser Bereich wird genutzt um zusц╓tzliche Informationen ц╪ber jeden Channel und Spieler anzuzeigen, erfordert aber JavaScript um\n" +" richtig angezeigt zu werden. Du wirst zwar die Detailseiten nicht sehen, kannst aber alle sichtbaren Formulare benutzen.\n" +" " + +#: templates/mumble/mumble.html:31 +msgid "Website" +msgstr "Webseite" + +#: templates/mumble/mumble.html:33 +msgid "Server version" +msgstr "Serverversion" + +#: templates/mumble/mumble.html:34 +msgid "Minimal view" +msgstr "Minimalansicht" + +#: templates/mumble/mumble.html:37 +msgid "Welcome message" +msgstr "Willkommensnachricht" + +#: templates/mumble/mumble.html:43 +msgid "Server registration" +msgstr "Benutzerregistrierung" + +#: templates/mumble/mumble.html:46 +msgid "You are registered on this server" +msgstr "Du bist auf diesem Server registriert" + +#: templates/mumble/mumble.html:48 +msgid "You do not have an account on this server" +msgstr "Du bist auf diesem Server nicht registriert" + +#: templates/mumble/mumble.html:57 +#, python-format +msgid "" +"\n" +"

You need to be logged in to be able to register an account on this Mumble server.

\n" +" " +msgstr "" +"\n" +"

Du musst eingeloggt sein um auf diesem Mumble-Server einen Account registrieren zu kц╤nnen.

\n" +" " + +#: templates/mumble/mumble.html:67 +msgid "" +"\n" +" Sorry, due to a bug in Murmur 1.2.2, displaying and setting the Texture is disabled.\n" +" " +msgstr "" +"\n" +"Entschuldigung, aufgrund eines Fehlers in Murmur 1.2.2 ist die Texturanzeige sowie das Hochladen fц╪r diese Version deaktiviert." + +#: templates/mumble/mumble.html:72 +msgid "" +"\n" +" You can upload an image that you would like to use as your user texture here.\n" +" " +msgstr "" +"\n" +"Du kannst hier ein Bild hochladen, das als deine Benutzertextur angezeigt werden soll." + +#: templates/mumble/mumble.html:77 +msgid "Your current texture is" +msgstr "Deine momentane Textur ist diese" + +#: templates/mumble/mumble.html:80 +msgid "You don't currently have a texture set" +msgstr "Du hast momentan keine Textur gesetzt" + +#: templates/mumble/mumble.html:84 +msgid "" +"\n" +" Hint: The texture image needs to be 600x60 in size. If you upload an image with\n" +" a different size, it will be resized accordingly.
\n" +" " +msgstr "" +"\n" +"Hinweis: Das Texturbild muss die Grц╤ц÷e 600x60 haben. Wenn du ein Bild mit einer anderen Grц╤ц÷e hochlц╓dst, wird es automatisch zurecht geschnitten.
" + +#: templates/mumble/mumble.html:103 +msgid "Server administration" +msgstr "Server-Administration" + +#: templates/mumble/mumble.html:117 +msgid "Player" +msgstr "Spieler" + +#: templates/mumble/mumble.html:119 +msgid "Online since" +msgstr "Online seit" + +#: templates/mumble/mumble.html:120 templates/mumble/player.html:9 +msgid "Authenticated" +msgstr "Authentifiziert" + +#: templates/mumble/mumble.html:121 templates/mumble/mumble.html.py:136 +msgid "Admin" +msgstr "Administrator" + +#: templates/mumble/mumble.html:122 templates/mumble/player.html:12 +msgid "Muted" +msgstr "Stummgestellt" + +#: templates/mumble/mumble.html:123 templates/mumble/player.html:18 +msgid "Deafened" +msgstr "Taubgestellt" + +#: templates/mumble/mumble.html:124 templates/mumble/player.html:21 +msgid "Muted by self" +msgstr "Selbst stummgestellt" + +#: templates/mumble/mumble.html:125 templates/mumble/player.html:24 +msgid "Deafened by self" +msgstr "Selbst taubgestellt" + +#: templates/mumble/mumble.html:127 +msgid "IP Address" +msgstr "IP-Adresse" + +#: templates/mumble/mumble.html:131 +msgid "User" +msgstr "Benutzer" + +#: templates/mumble/mumble.html:134 +msgid "Full Name" +msgstr "Vollstц╓ndiger Name" + +#: templates/mumble/mumble.html:137 +msgid "Sign-up date" +msgstr "Datum der Anmeldung" + +#: templates/mumble/mumble.html:142 +msgid "User Comment" +msgstr "Benutzer-Kommentar" + +#: templates/mumble/mumble.html:154 templates/mumble/mumble.html.py:168 +msgid "Kick user" +msgstr "Benutzer kicken" + +#: templates/mumble/mumble.html:160 +msgid "Reason" +msgstr "Begrц╪ndung" + +#: templates/mumble/mumble.html:165 +msgid "Ban user" +msgstr "Benutzer bannen" + +#: templates/mumble/mumble.html:175 +msgid "Channel" +msgstr "Kanal" + +#: templates/mumble/mumble.html:177 +msgid "Channel ID" +msgstr "Kanal-ID" + +#: templates/mumble/mumble.html:179 +msgid "Connect" +msgstr "Verbinden" + +#: templates/mumble/mumble.html:182 +msgid "Channel description" +msgstr "Beschreibung des Kanals" + +#: templates/mumble/mumble.html:229 +msgid "Delete" +msgstr "Lц╤schen" + +#: templates/mumble/mumble.html:265 +msgid "Server Info" +msgstr "Server-Infos" + +#: templates/mumble/mumble.html:266 +msgid "Registration" +msgstr "Registrierung" + +#: templates/mumble/mumble.html:275 +msgid "Administration" +msgstr "Administration" + +#: templates/mumble/mumble.html:282 +msgid "User List" +msgstr "Benutzerliste" + +#: templates/mumble/mumble.html:286 +msgid "name" +msgstr "Name" + +#: templates/mumble/mumble.html:306 +msgid "Change password" +msgstr "Passwort ц╓ndern" + +#: templates/mumble/mumble.html:319 +msgid "Add" +msgstr "Hinzufц╪gen" + +#: templates/mumble/mumble.html:331 +msgid "Save" +msgstr "Speichern" + +#: templates/mumble/mumble.html:357 +msgid "Resync with Murmur" +msgstr "Liste neu laden" + +#: templates/mumble/player.html:15 +msgid "Suppressed" +msgstr "Unterdrц╪ckt" + +#: templates/mumble/player.html:27 +msgid "has a User Comment set" +msgstr "hat einen Benutzer-Kommentar gesetzt" diff --git a/mumble/locale/fr/LC_MESSAGES/django.mo b/mumble/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..f0df7e8295ad3d04c7ccf1edc9da49f0d85ac8f7 GIT binary patch literal 10574 zcma)>ZHyh)S;tSBq)FV8q-~Nm1f&Uw!B{v7|q+pqdd6AztgD-<01HTEfb+em6CAb&VdS3uF{wsC=Pr-fkzXfXj zAA$Pr1{Q@i=2q}d@J>+QErD+XKLTpL6#N-*8`SvEfZETOK#hM156sj1r$N!RQTIOq zijH3gHScqv*7-8XpZPjJY~B1VD8By_coq0>p!V@Y@D}jLp!jnOA4|@*#%WOBAE^7s zK$@QOG&l=tU%v@zU!MWB?&m=5=MU@tAJzD0pycz{pw{_&P~X1_ zYTy3@YQI;%GvLjj>}NNqb?*Z20v`jVKNZNI`7A$tYF+|G$G5>Jq5E&ZuhGBf)!-?p08IF+=jc5&R%{2Piphftv5XivK?XrFYk{dCg}*$#*X(x*q|x-s7P5 z{|qSk{!-olT~PDB0BV2V05$%*;8ozO;N9SVf-Gfj!*~yZM?oH$XF=)7?}PgO&%ryd zFy{NG2+nSoi>_@sEO#ZI07)ouZwkJwy|Ix=LD? z)}viRdzjX~9-t#0Iobi*fL74NuZ+eLe$Vd%QFAj-`zX!QZlkTybRD8?(!^g+Gx!uu z*IwEp?P_(nqyrt=2h`z`4r$*XriowDl{uQOIDokie3B;loTeS2HP=VDksPFx_cRZ| z6|CZ{#mK5p)&Jx%)$U%1ifCC$4R=;-|ik<%kADn6SJ6! z-V-d`W64VwFE^voQ*|z@`myaNXW3Y0tHRl+(2HF6`DnA75Y=tX++JlR%k%AN8&`gc z$gWiz+Wb$`ZoRI?C#_976U zCw9HMnG`n4U1ULMjka2(N?t5%adUlgj=(qo}a~#MecefvasT{7gm}fJ_k40TDwW?QiQps z3tlARHKOe&=~;FFQ$%1OA`3O(3zdH3MnVbC-) z*Ni)ty4=jgF>R2f;a6Yh862p28=kU8?s;$K`u*&jJ+`{ESX1p~hQ173QHV=s4*sEU zHV6bBA>4R-M5(qb?MzcWWv1ys~qSD3Yuv8s-brYXU;&Rh|`0%j!s5vZ(R{TcV z_x2ngH=0_MQC=2i9+hD5h%q%8GS>{%h>&GboWlffGxMkqKA?Hv`>0cccHzkA)^m2$ zi(TQB3S}HU(|)w7J8$MQw6a&4BZsTPqN@w@N9|fvICI2B>n>%Z&5d2#Mn3&@9;`=I zf6Vn!KTFq>4PN(M>CDmd(O}qj#jKr5UD++L^PWTPHe7e$g$>ARr@VxtSXPvNL!y3Jc8E06 zwGfY?4(a!DR~o!=mQDwvG!!Gx*1f6r`U#rkor#qO1hZk<6wOGP0h7q9G{tY3$CBRJ zplOSkZ(}(|9l14c56#m;A8JCZJv)aXn?+gYSZ$UfZ-K4pm!k7sdy35!hT8<$*uG0Q zP-oGKzZ_f3Qj!vh`gSXE=S*#}wW3e-m?@ZZ(C4R;xOpIhvAg#Ss=HuC%3YCf=@#pb zrC&$RpyfU`XjsfJzLd53-;o9$&&=_x5lB+=tgxHWmP3}9Sv$eGb0WnZ;&KxB-E{Q6Fw3>oVItl)d#86MOVX}gbXvm=hUK=`+hMiVPkJ>eMb|8Q z&t8M~8?~J+XE}ZZws-~%>806SILaU{Sv+Er7q2B}b8qkPDoOn2`V(Qr@DJ2MDa@>aa^Nu6k~l`Cu@0$6 z5h5g`P?b73Het)1XKNw8w`L#U;LcP9PDbtn7pLtJ=49WvDZEy?p;gDh#b;~yCw!yV zP%I>*9$}dFH&tL`g8_pko^3|OrXGm$#Nlqa$^~T#d{FJJP9wt@%pvMedj=};$_CI4sZBAb;_-SCI~;q=N`@U9Rl15yL#$V&<@!JS&OwEHlVr zA77$g$)>w@xsRcve3G|czFuNjdqJj6(9fzgr+DLk?0k@Y=q$p{a95|-p;K$;M5hQn zuiX-pk51-sw^k%rPmT7}$wlt_D44TLAypgo>(tFW;R>UiREiWN>b14WC&M26h&c0% z_QBybGS7*T=@e~ZJ$GIzd?=4~jV zyLKh9TNzn!C}7H;;@ydr!lK77Ci7} z_aMS2d+`JTHWFz>3Xtr z(O2D`AtEA6as_+JKlaG>aHvC=UdV;jnFP1N6^%=7+!cYt?~2c2P7wTOW{#|o5MkN{ zxTZo&0^9SbhrPPF-}qWQuiEgVQ`ClTgbcyM0h?`D0NK@?>W|fp<{rkZx)Vt zX8Zf&* zOjL0m2Qp;w;uIx*slx#(`YI&86K_a#k;#CmkDjL*`hV5=UN0L;-l)*`gMFnBFR0(p z4)~E_W!R$PzAwI=ifi-({$@S73xtL{l(@;%@C2yP5Q)n}`(vm^#e%#7%SO}o*g6Mj z-hV5QdKpWM{m7U75(o|`%rAW$(}0=O&;8Lrnx)%B{?=Il!6mDbY3~JU+6xh{8Cz=O zT@5qIyLwl{c^Y1k#TKWUvV@`_C3ll>{ow$6nY4mMaYAMp40n+w`U&nSV=GcA|G1#C zq8O5E^S)fGsZ_+$!h%aJ-8GA2f6-d~RNGv2kic;RNrH@xI@O;gXn&2J`;@>H<}{vK zbr@#}_=&mz9C@NpbX9qCDCiACF}%elxY6p>F046NkW*?-7JT7S3^Q^~)MR3g=CEOO z&OpA_7{s8WbDZjKuIl+LpgETr%h{$=k;O~mPMXt(QTXcDvZ6Th6SO64%N*BCz%)lw z#IgbZV>hhGAt$&=#php|wiw7NIFKOtC`J;?M+VNp1oB01sNm&q^d5gytE3I56y5d; zs_S3Fd9=7%Na(%>+Pz2M&nwa?Z&K(=@Zua%_LUlpHOEQ}uX|>3yds;(IW@1R9`m!= z!iClr!l?zj()2BdJ$Z(~`!&h*Mx!AP!2Q9JSmCQ%}PY}wh`sAQ>65TzZ`ZgiPw1K-Iq^y_#(uBX0T2;n*T>= z&EcKlc*+R{X$@Oe`P%Axb05X!_M3&vwR?0DiJ-?zF|avH9_^?uY1+e7rA!1z&)i8m zIV5e)O3ae1jU1rPPdI($bX*6d;tesn|4gN%HJ(BZ_`e|ymFJcw@?T|+|Dlqz^e(Sv zogY(A=TAkTCu9=jhJ&f7S)fkuuovZ7xJ(kX9504{YI#3mInkhAZ*&wgA8KHqK%&kQ zzQr8lAVf+&`BZz{y~9z7I;uT$$e|kK}rIOtgm-><>r**f+-_UHe86Fc>aM zT4z_69*VGU?-gDQE66Ue39)7Jcleu-1ao-7F(bAnue`2z{Z1iWUaN!` z-{_>|lIJ=pVN&+VX8Zxkkn<8K)k7quB4@qJVMI=q1}nTmT$aeSY@l^$GLDhtQ~t;- zs7yP8ahgvcWai0az|4iR^>RfDnS7JY_=H)rlio{iEiWEYr}h)SOZ~3^N0+R5&aD{^ zTs-Yfzdm%)PS>zQlEliH*L--QN{p{mg5F}Fqgv#WIwK4tXv)$gP$sV}bh041d|*?( d2L*0oE8|pE)RjF_GLfu>Ii&{TvB682{{ylS#KQmp literal 0 HcmV?d00001 diff --git a/mumble/locale/fr/LC_MESSAGES/django.po b/mumble/locale/fr/LC_MESSAGES/django.po new file mode 100755 index 0000000..3d14119 --- /dev/null +++ b/mumble/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,568 @@ +# French translation file for Mumble-Django. +# +# Copyright (C) 2009, Antoine Bertin +# +# Mumble-Django is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This package is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +msgid "" +msgstr "" +"Project-Id-Version: Mumble-Django v2.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-03-14 18:23+0100\n" +"PO-Revision-Date: 2010-03-14 23:10+0100\n" +"Last-Translator: Antoine Bertin \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Translated-Using: django-rosetta 0.5.3\n" + +#: admin.py:34 +#: admin.py:51 +msgid "Master is running" +msgstr "Le serveur fonctionne" + +#: admin.py:59 +#: models.py:162 +#: templates/mumble/mumble.html:28 +msgid "Server Address" +msgstr "Adresse du serveur" + +#: admin.py:66 +#: models.py:165 +msgid "Server Port" +msgstr "Port du serveur" + +#: admin.py:71 +msgid "Instance is running" +msgstr "L'instance fonctionne" + +#: admin.py:81 +msgid "Registered users" +msgstr "Utilisateurs enregistrц╘s" + +#: admin.py:91 +msgid "Online users" +msgstr "Utilisateurs en ligne" + +#: admin.py:101 +msgid "Channel count" +msgstr "Nombre de canaux" + +#: admin.py:108 +msgid "Yes" +msgstr "Oui" + +#: admin.py:110 +msgid "No" +msgstr "Non" + +#: admin.py:114 +msgid "Public" +msgstr "Public" + +#: admin.py:132 +#: models.py:631 +#: templates/mumble/mumble.html:223 +msgid "Admin on root channel" +msgstr "Admin sur le canal racine" + +#: forms.py:83 +msgid "Password required to join. Leave empty for public servers." +msgstr "Mot de passe requis pour se connecter. Laisser vide pour un serveur public." + +#: forms.py:86 +msgid "If on, IP adresses of the clients are not logged." +msgstr "Si oui, les adresses IP des utilisateurs ne seront pas loggц╘es." + +#: forms.py:142 +#, python-format +msgid "Port number %(portno)d is not within the allowed range %(minrange)d - %(maxrange)d" +msgstr "Le port %(portno)d n'est pas dans la plage autorisц╘e %(minrange)d - %(maxrange)d" + +#: forms.py:152 +msgid "Default config" +msgstr "Configuration par dц╘faut" + +#: forms.py:165 +#: templates/mumble/offline.html:12 +msgid "This server is currently offline." +msgstr "Ce serveur est offline." + +#: forms.py:190 +msgid "That name is forbidden by the server." +msgstr "Ce nom est interdit par le serveur." + +#: forms.py:193 +#: models.py:587 +msgid "Another player already registered that name." +msgstr "Ce nom d'utilisateurs est dц╘jц═ pris." + +#: forms.py:201 +#: forms.py:307 +#: models.py:589 +msgid "Cannot register player without a password!" +msgstr "Impossible d'enregistrer un utilisateur sans mot de passe!" + +#: forms.py:213 +#: models.py:179 +msgid "Server Password" +msgstr "Mot de passe du serveur" + +#: forms.py:214 +msgid "This server is private and protected mode is active. Please enter the server password." +msgstr "Ce serveur est privц╘ et le mode protц╘gц╘ est actif. Merci de saisir le mot de passe du serveur." + +#: forms.py:222 +#: forms.py:274 +msgid "The password you entered is incorrect." +msgstr "Le mot de passe saisi est incorrect." + +#: forms.py:237 +msgid "Link account" +msgstr "Lier le compte" + +#: forms.py:238 +msgid "The account already exists and belongs to me, just link it instead of creating." +msgstr "Ce compte existe dц╘jц═ et m'apartient. Vous pouvez le lier." + +#: forms.py:255 +msgid "No such user found." +msgstr "Aucun utilisateur trouvц╘." + +#: forms.py:290 +msgid "That account belongs to someone else." +msgstr "Ce compte appartient ц═ quelqu'un d'autre." + +#: forms.py:293 +msgid "Linking Admin accounts is not allowed." +msgstr "Les liaisons de comptes Admin ne sont pas autorisц╘es." + +#: forms.py:322 +#: templates/mumble/mumble.html:65 +#: templates/mumble/mumble.html.py:148 +#: templates/mumble/mumble.html:278 +msgid "User Texture" +msgstr "Avatar de l'utilisateur" + +#: models.py:63 +msgid "DBus or ICE base" +msgstr "DBus ou ICE base" + +#: models.py:64 +msgid "Examples: 'net.sourceforge.mumble.murmur' for DBus or 'Meta:tcp -h 127.0.0.1 -p 6502' for Ice." +msgstr "Exemples: 'net.sourceforge.mumble.murmur' pour DBus ou 'Meta:tcp -h 127.0.0.1 -p 6502' pour Ice." + +#: models.py:65 +msgid "Ice Secret" +msgstr "Ice Secret" + +#: models.py:68 +#: models.py:159 +msgid "Mumble Server" +msgstr "Serveur Mumble" + +#: models.py:69 +msgid "Mumble Servers" +msgstr "Serveurs Mumble" + +#: models.py:160 +msgid "Server Name" +msgstr "Nom du serveur" + +#: models.py:161 +msgid "Server ID" +msgstr "ID du serveur" + +#: models.py:163 +msgid "Hostname or IP address to bind to. You should use a hostname here, because it will appear on the global server list." +msgstr "Nom de domaine ou adresse IP ц═ associer au serveur. Il est prц╘fц╘rable d'utiliser un nom de domaine car il sera visible dans la liste des serveurs." + +#: models.py:166 +msgid "Port number to bind to. Leave empty to auto assign one." +msgstr "Numц╘ro de port du serveur. Laissez vide pour assigner automatiquement." + +#: models.py:167 +msgid "Server Display Address" +msgstr "Adresse du serveur ц═ afficher" + +#: models.py:168 +msgid "This field is only relevant if you are located behind a NAT, and names the Hostname or IP address to use in the Channel Viewer and for the global server list registration. If not given, the addr and port fields are used. If display and bind ports are equal, you can omit it here." +msgstr "Ce champ n'est valable que si vous ц╙tes derriц╗re un NAT. Nom de domaine ou adresse IP ц═ utiliser dans le Channel Viewer et pour la liste des serveurs. Si vide, les champs d'adresse et de port sont utilisц╘s. Si le port d'affichage et le port du serveur sont ц╘gaux, vous pouvez laisser vide." + +#: models.py:174 +msgid "Superuser Password" +msgstr "Mot de passe de Superuser" + +#: models.py:177 +msgid "Website URL" +msgstr "URL du site web" + +#: models.py:178 +msgid "Welcome Message" +msgstr "Message de bienvenue" + +#: models.py:180 +msgid "Max. Users" +msgstr "Utilisateurs Max." + +#: models.py:181 +msgid "Bandwidth [Bps]" +msgstr "Bande passante [Bps]" + +#: models.py:182 +msgid "SSL Certificate" +msgstr "Certificat SSL" + +#: models.py:183 +msgid "SSL Key" +msgstr "Clц╘ SSL" + +#: models.py:184 +msgid "Player name regex" +msgstr "Regex pour le nom des utilisateurs" + +#: models.py:185 +msgid "Channel name regex" +msgstr "Regex pour le nom des canaux" + +#: models.py:186 +msgid "Default channel" +msgstr "Canal par dц╘faut" + +#: models.py:187 +msgid "Timeout" +msgstr "Timeout" + +#: models.py:189 +msgid "IP Obfuscation" +msgstr "IP Anonyme" + +#: models.py:190 +msgid "Require Certificate" +msgstr "Certificat requis" + +#: models.py:191 +msgid "Maximum length of text messages" +msgstr "Longueur maximum des messages textes" + +#: models.py:192 +msgid "Allow HTML to be used in messages" +msgstr "Permettre le HTML dans les messages" + +#: models.py:193 +msgid "Publish this server via Bonjour" +msgstr "Publier ce serveur via Bonjour" + +#: models.py:194 +msgid "Boot Server when Murmur starts" +msgstr "Dц╘marrer ce serveur au lancement de Murmur" + +#: models.py:212 +#: models.py:213 +msgid "Boot Server" +msgstr "Serveur dц╘marrц╘ automatiquement" + +#: models.py:217 +#: models.py:550 +msgid "Server instance" +msgstr "Instance du serveur" + +#: models.py:218 +msgid "Server instances" +msgstr "Instances du serveur" + +#: models.py:510 +#: models.py:690 +msgid "This field must not be updated once the record has been saved." +msgstr "Ce champ ne dois pas ц╙tre modifiц╘ une fois l'enregistrement sauvegardц╘." + +#: models.py:547 +msgid "Mumble player_id" +msgstr "ID de l'utilisateur" + +#: models.py:548 +msgid "User name and Login" +msgstr "Nom d'utilisateur et login" + +#: models.py:549 +msgid "Login password" +msgstr "Mot de passe" + +#: models.py:551 +#: templates/mumble/mumble.html:293 +msgid "Account owner" +msgstr "Propriц╘taire du compte" + +#: models.py:553 +msgid "The user's comment." +msgstr "Commentaire de l'utilisateur" + +#: models.py:554 +msgid "The user's hash." +msgstr "Hash de l'utilisateur" + +#: models.py:558 +msgid "User account" +msgstr "Compte d'utilisateur" + +#: models.py:559 +msgid "User accounts" +msgstr "Comptes d'utilisateur" + +#: models.py:566 +#, python-format +msgid "Mumble user %(mu)s on %(srv)s owned by Django user %(du)s" +msgstr "L'utilisateur Mumble %(mu)s sur %(srv)s correspond ц═ l'utilisateur Django %(du)s" + +#: templates/mumble/list.html:20 +msgid "No server instances have been configured yet." +msgstr "Aucune instance du serveur n'a ц╘tц╘ configurц╘e pour l'instant." + +#: templates/mumble/mumble.html:16 +msgid "" +"\n" +" Hint:
\n" +" This area is used to display additional information for each channel and player, but requires JavaScript to be\n" +" displayed correctly. You will not see the detail pages, but you can use all links and forms\n" +" that are displayed.\n" +" " +msgstr "" +"\n" +" Remarque :
\n" +" Cette zone est utilisц╘e pour affichier des informations supplц╘mentaires pour chaque canal et utilisateur.\n" +" JavaScript est nц╘cessaire pour un meilleur affichage. Vous ne verrez pas les pages de dц╘tail mais vous pouvez\n" +" utiliser les liens et les formulaires.\n" +" " + +#: templates/mumble/mumble.html:31 +msgid "Website" +msgstr "Site web" + +#: templates/mumble/mumble.html:33 +msgid "Server version" +msgstr "Version du serveur" + +#: templates/mumble/mumble.html:34 +msgid "Minimal view" +msgstr "Vue minimaliste" + +#: templates/mumble/mumble.html:37 +msgid "Welcome message" +msgstr "Message de bienvenue" + +#: templates/mumble/mumble.html:43 +msgid "Server registration" +msgstr "Inscription serveur" + +#: templates/mumble/mumble.html:46 +msgid "You are registered on this server" +msgstr "Vous ц╙tes authentifiц╘ sur ce serveur" + +#: templates/mumble/mumble.html:48 +msgid "You do not have an account on this server" +msgstr "Vous n'avez pas de compte sur ce serveur" + +#: templates/mumble/mumble.html:57 +#, python-format +msgid "" +"\n" +"

You need to be logged in to be able to register an account on this Mumble server.

\n" +" " +msgstr "" +"\n" +"

Vous devez ц╙tre authentifiц╘ pour pouvoir inscrire un compte sur ce serveur Mumble

\n" +" " + +#: templates/mumble/mumble.html:67 +msgid "" +"\n" +" Sorry, due to a bug in Murmur 1.2.2, displaying and setting the Texture is disabled.\n" +" " +msgstr "" +"\n" +" Dц╘solц╘, ц═ cause d'un bug dans Murmur 1.2.2, l'affichage et le paramц╘trage des avatars sont dц╘sactivц╘.\n" +" " + +#: templates/mumble/mumble.html:72 +msgid "" +"\n" +" You can upload an image that you would like to use as your user texture here.\n" +" " +msgstr "" +"\n" +" Ici vous pouvez uploader l'avatar que vous souhaitez utiliser.\n" +" " + +#: templates/mumble/mumble.html:77 +msgid "Your current texture is" +msgstr "Votre avatar est" + +#: templates/mumble/mumble.html:80 +msgid "You don't currently have a texture set" +msgstr "Vous n'avez pas d'avatar pour l'instant" + +#: templates/mumble/mumble.html:84 +msgid "" +"\n" +" Hint: The texture image needs to be 600x60 in size. If you upload an image with\n" +" a different size, it will be resized accordingly.
\n" +" " +msgstr "" +"\n" +" Remarque : La taille de l'avatar doit ц╙tre de 600x60. Si vous uploadez une image\n" +" d'une taille diffц╘rente, elle sera redimensionnц╘e.
\n" +" " + +#: templates/mumble/mumble.html:103 +msgid "Server administration" +msgstr "Administration du serveur" + +#: templates/mumble/mumble.html:117 +msgid "Player" +msgstr "Utilisateur" + +#: templates/mumble/mumble.html:119 +msgid "Online since" +msgstr "En ligne depuis" + +#: templates/mumble/mumble.html:120 +#: templates/mumble/player.html:9 +msgid "Authenticated" +msgstr "Authentifiц╘" + +#: templates/mumble/mumble.html:121 +#: templates/mumble/mumble.html.py:136 +msgid "Admin" +msgstr "Admin" + +#: templates/mumble/mumble.html:122 +#: templates/mumble/player.html:12 +msgid "Muted" +msgstr "Muet" + +#: templates/mumble/mumble.html:123 +#: templates/mumble/player.html:18 +msgid "Deafened" +msgstr "Sourd" + +#: templates/mumble/mumble.html:124 +#: templates/mumble/player.html:21 +msgid "Muted by self" +msgstr "Devenir muet" + +#: templates/mumble/mumble.html:125 +#: templates/mumble/player.html:24 +msgid "Deafened by self" +msgstr "Devenir sourd" + +#: templates/mumble/mumble.html:127 +msgid "IP Address" +msgstr "Adresse IP du serveur" + +#: templates/mumble/mumble.html:131 +msgid "User" +msgstr "Utilisateur" + +#: templates/mumble/mumble.html:134 +msgid "Full Name" +msgstr "Nom" + +#: templates/mumble/mumble.html:137 +msgid "Sign-up date" +msgstr "Date d'enregistrement" + +#: templates/mumble/mumble.html:142 +msgid "User Comment" +msgstr "Commentaire d'utilisateur" + +#: templates/mumble/mumble.html:154 +#: templates/mumble/mumble.html.py:168 +msgid "Kick user" +msgstr "Kicker l'utilisateur" + +#: templates/mumble/mumble.html:160 +msgid "Reason" +msgstr "Raison" + +#: templates/mumble/mumble.html:165 +msgid "Ban user" +msgstr "Bannir l'utilisateur" + +#: templates/mumble/mumble.html:175 +msgid "Channel" +msgstr "Canal" + +#: templates/mumble/mumble.html:177 +msgid "Channel ID" +msgstr "ID du canal" + +#: templates/mumble/mumble.html:179 +msgid "Connect" +msgstr "Se connecter" + +#: templates/mumble/mumble.html:182 +msgid "Channel description" +msgstr "Description du canal" + +#: templates/mumble/mumble.html:229 +msgid "Delete" +msgstr "Supprimer" + +#: templates/mumble/mumble.html:265 +msgid "Server Info" +msgstr "Information du serveur" + +#: templates/mumble/mumble.html:266 +msgid "Registration" +msgstr "Enregistrement" + +#: templates/mumble/mumble.html:275 +msgid "Administration" +msgstr "Administration" + +#: templates/mumble/mumble.html:282 +msgid "User List" +msgstr "Liste des utilisateurs" + +#: templates/mumble/mumble.html:286 +msgid "name" +msgstr "nom" + +#: templates/mumble/mumble.html:306 +msgid "Change password" +msgstr "Changer le mot de passe" + +#: templates/mumble/mumble.html:319 +msgid "Add" +msgstr "Ajouter" + +#: templates/mumble/mumble.html:331 +msgid "Save" +msgstr "Sauvegarder" + +#: templates/mumble/mumble.html:357 +msgid "Resync with Murmur" +msgstr "Synchroniser avec Murmur" + +#: templates/mumble/player.html:15 +msgid "Suppressed" +msgstr "Muet" + +#: templates/mumble/player.html:27 +msgid "has a User Comment set" +msgstr "a un commentaire" + +#~ msgid "comment" +#~ msgstr "commentaire" + +#~ msgid "hash" +#~ msgstr "hash" diff --git a/mumble/locale/it/LC_MESSAGES/django.mo b/mumble/locale/it/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..7ebc3e5f363c9094b2ed45eb48a1c318038dbf14 GIT binary patch literal 10087 zcmai(ZH!#kS;tSB#7P!flG4y7q0LDkah&XockRS+wvOrA>(rZgZD)7wHc3OcGjn$K z+IQ}q+#8)WsK^IUTUBYrOVvPIQBc)_R1gG}esB;U03jmrr79?u5CS0tLLyLv z5WoL9=g!RLg!wyy?TQ&e+d2n zbl|Vf7_+G_e#DrYt}-SCpQHQnA2sHk;QxS3VP@!D3%(1y8GH};F7RIP4)7sx7JLfi z&pgk^_23u49|OM(GPU^@_yO>b!F$2)#qY0WGHvl@kS&?_gM4M~0e>7k1il-56x6t< z!5cvbYMrgv|0JmOJ`3&vUj%;^{4J0pn>~D4gExX&=hGlxna{@lm%(}ZzXNLB?}K{p z-#|oR{u{g-ypF~7UK_j#d>GVxXTaORCqa$B4C)-e2x|NbAb;kod^`mHJ}9~VE2we* z0lovgnn_#W9#H$ZAJlv&Bc1~n=yyP!^GhIq=Bs?Hf`0%$2;P9u^!*~JeVvT`O^~Hb z0czgoK&|u3Ab;jJ_~5AK+o0tAGI$O67og7Z*WgcpuY!`#e}dBQRZJp$C#d&tiT!<` zc=JGf{urqBp91+a86TSWIZ*rg49Jw`*FnkkTi{*bOA%iM&(PoVo}iEOAdk!^K+XF( z@V)T)H^HNNkHy8)AA;w>+t|F$Rf0O#Wl-yW64ZHqDfYh*@hhP8^Shwd`4dp@zXIyK z-vf2te~9=RD7)G7-mvZrxR3rUDE^!W`7@v3gQ4aHQ2Y1>xSxIh9{5H2KX{)pkApk6 z1Rno6c!cloz15ftJn!FT3`5K-pyc$=pyc=(D0yFt6VSeH1LY6y<3sW|33^_FdhZ2L z^SlV^Jbwh<3H~WK2mT`{ecbZ?z^7SI?;ivu??q7OSdIM@lzcA7{-;3k{PUpZ{}oXC z{2HkBzYRVKeixJ+ZpBG_8oV7u6y|FYzXNLg-+)KKS3&LjcF3)H4};p*5;za8f?DSn zKtyIf4{F>uLGj@w5K)=$gL?13K#kvH!}y!Q`{>Vs+SkWGy`O-x(*bw@9D+LkS3vRS zB~bId44wdA0rmWSEGquo2|flM0++x~fs)ssf#Szs#r|vHP4xd4)OzXdh^UqG#U{Z9qF71VsQp!UBA zieKkI@!}$=`F|d~3w$2r&wP^)?ei5-=lUm5@7?&*VcxsJf2V&KJOkdx%iNlegC7N7 z1SOXrg7RZmF^TN-dQiN(8PtC62DOg`Q1h?E=Uq_oVH?!==Rob}a}mD|UQPeIparjh zTL14sL||^%8{{<$LQS&-UITW(tH2eSu65c4+RxB1QGZo@wrCyNb+i+->FY2Zt4+VS@tUK>skw74uRtLkaj*k6Nnor?G)_* z?GBoF@pH5R?Gc(TN4rQ{qKTKf#BXs;*GZaqwMU(JJr3d~&4-%@-~&_l3-SIP;8~jd z*9PrA8gw$x(wb|In|-urXiw8*d;XfSyRMUIUN6}7O=oL&sUDTirh}yK?2*pV+__$L zWPj(Vtqa?6_R!qir9*Qz&25!lc5S=7VRwp=9SyT0>DeTYZ*Hgc=Ia(oY%kr|aHY#@ z|L&|!YrCChnWinBrelI`w?h5e{|O@W>@bC7Vfg->ld##liKfUyBK9X zo23^ySYbz%vq_~dO5K;^!)~&v_SDSNtE?4exif2fBR@rAJEOi1zcMNZOf%m;&_2Mo zX*JA}9kz`GdbVda5 zHtF@!IxX@fV@DfBIY`vA+*y}&H*I$_$#a+aEopnMoMrnpvVL}yB5?a?vX!iL%XC-= z4QLQNUMdBSv>}KgU9=`Lgd$OXR=Fg1EY+rTfQ96AFrTLNlNkdYJ zS+A9@pH?-Rh9qLv2Un$n?G4=H-T|Rh^V|WsXA$29E_3 zw;^*aphki&t7;n)yu%!aKKOvhMB7t?B3kf z?Fz&0I(XK1?Sa=WAT!a}R!9FTvh`&Kc2y(M|N8SbFaNR^-G(ImG4qZ|P zHagqSiVkKR$cWKYb=$BI?9)QJJ=573Rq#MUWp}8%Te{ldev5oIY)V71>SDv&W;aWr zj`to`Di3zTv^iABnE@LpM|qB;GLNO*i-B6Fu-C>^j5_jT-T<1Xm0r|@n0K*_<(gA6 z#|ckX5^r)-)2}3#+V&iWs|>d(y0Mwd`|wwL#X(NYV(}4F)gYNOIsOkHlOpe?qwgzoHkuhW;hnMf zau_hIO_maU6)n3t&huFZSUXgSuqm zs7Y=+YR;D4z>!s&`&$OLB)a5M?3(E*75^KyPw$cE$w(hnmG*OFj_I8Bdk#=-28YYJ z@w8|*vB!%10>)}qT~fj4)##=XWspvI5Pj-ukdQs@$~xWfzK}PnoxDpxf8%ME=I$lH~yYkSMX>;UB_ib-uHg0 zDh3YCSeI3($B*(34RYWxJ87>6rLhjtqY5FBQBWmD!zNC-OB^i(^r_hgIJgU=3MV7? zfs51eh-{tni@|i-r4Sm-h7H`m9e!bb2JimfqUAvkT>V8l+&TTY@Tazn1{4I_qSy2Z@lFlA8_*jZ*!^n83tx;2Mx+p`&l4*8^SzkFO`7`-6VrtouA zn{#~QKkV_K`_MUsIKy4cr$Z;Qb62MdJNi}zk%=ePVjO7|$8CN1y4w|rRMvT8m zDO`Z~O4Uz$tz)Bp z)mkqW?9ACS>#gJ7OpCm=ggO`Ofw}p)*4)9?{Jfn%v~d5y`{>Uxu(j&8QaPp9kGtQ_ zA6$4~e&!U>zqL;MsIsIk$_0DGJH!w9&vwrpomp94IWgJge0y$&jIf3qt@WKDODiGR zPgqa$4=T+rIpo9V)*opdo{ZDYqAJa2~ufwYsd zdN(Nmort+>K*07rHfmf z9F3)Yuz?Rz{HM_y{x7u@v(&V8($7-rY?rYZ#Vz#k>kLOlYR4yF!}SP3*ffMaWaWI7 z+pO3{Rk-|qkrz|vtkb6c9}91IbP1~p%#(KhpHerLxjJ1FWXOtaHL}kJQ4c~|99+(% zs>MOzswsTRaIhw@n?-SwIqpr2lO(N+b-Xttiq88@dAx!$Dw6{}N|C1rop!BWw=gO^X< zrV=J(<~V8D;Fc25m@`W6Y-nO}&FbV`kTz!lnR64SsELz7hu5)`=9wyLIBAxb#1|WT zkF#E#)|}AGE;XSh4o0PML8N9?6|v{He=*%v9Jg$t;<)9DV}6I~6=szm>Pmj`VSjO9 zR`s>9x?Kp1BQPf_s)zLLt75SzRK+$F#g?jgx+osk#2U#~RORd{hvgpfURvTY*y~v` zRFwfmH)R`mNc>c>Rl1HckV-BJa*yj`>YiC0y}Nnkw=LfytX(z=^N_P@;Uw0_`oT$h z@xRzm$WAYeOxMCMApF8Y4JjXnLzxUQ`kf(G&A4vVPWIrcf}bJdsMMc;6+-Ep64T`t zf&n+ozzgY?i4g3A!YUy-HsC2|0Wg%xvR&I z5P{)i*Q>KfJ6eoYLM*%SMornRhoDko6{V5M7WWz^UG56__q-1 zL+qB(s3lh1+(ShFN(^%rqX_XRR@}YB_*GMt2DtC3dfUeeaY~%Vo0Oiaq%EGbc576` z(9|%^Qh=)hXVDKgVaH4iw~~8j4yr86*WO|f`eNsJ6C470B1iMpwN<74mWB7} z(ZJz1B?v#~Zo_($-Kqp{p>Nm6JIK(WP^iKSp$?EVjy6~jLQAL}+_)NF!j5srjpQbb z*J)8r8FB#Q=^<)QSLAC{aCjzfc2fGJDsN4O6{^x($2U`7lPB3$GG)a&Yh>QER|$`l zb?B#_uz1;8UgAytr0n;kd}#c#T-0Hs!y{#fZr^J9$_>!E>r{r~zYEq8B&N1)G zb*IvjSdxb{@l7K}&}Q4t%;5N0V$6W1%HI9d*W2FjjQYxkXLXcGjsU$vzC!>T3qKrL z=FSPp6AAFj6qKO$3WT9tP+=~(JuyFe88%E}hLheZvN|E>$*7){mX6b+z&!0?T!e=M z=|#;m>+=y6-;uy+lSO$Ygk-&Km(o7}UBpYSUrc8(6U9%J;UR`i7n=k$c}>Lc-Ms9rG`xh~Ly^8F)^59wp*r^#OvxS|gc2QB>k~eg z6P59I<}DttUHJxnVk`xTdXgYV*%5Zo)!Pl4(uQ=adQSpqiY(RNc>NlEBW1V(k)lw{ TN#%}s{)y#88L?^|@~Hm +# +# Mumble-Django is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This package is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +msgid "" +msgstr "" +"Project-Id-Version: Mumble-Django v0.8\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-03-14 18:23+0100\n" +"PO-Revision-Date: 2010-03-14 13:51\n" +"Last-Translator: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Translated-Using: django-rosetta 0.5.3\n" + +#: admin.py:34 admin.py:51 +msgid "Master is running" +msgstr "Il Master ц╗ in esecuzione" + +#: admin.py:59 models.py:162 templates/mumble/mumble.html:28 +msgid "Server Address" +msgstr "Indirizzo del Server" + +#: admin.py:66 models.py:165 +msgid "Server Port" +msgstr "Port del Server" + +#: admin.py:71 +msgid "Instance is running" +msgstr "Istanza ц╗ in esecuzione" + +#: admin.py:81 +msgid "Registered users" +msgstr "Utenti registrati" + +#: admin.py:91 +msgid "Online users" +msgstr "Utenti online" + +#: admin.py:101 +msgid "Channel count" +msgstr "Quantitц═ canali" + +#: admin.py:108 +msgid "Yes" +msgstr "Sц╛" + +#: admin.py:110 +msgid "No" +msgstr "No" + +#: admin.py:114 +msgid "Public" +msgstr "Publico" + +#: admin.py:132 models.py:631 templates/mumble/mumble.html:223 +msgid "Admin on root channel" +msgstr "Admin nel canale root" + +#: forms.py:83 +msgid "Password required to join. Leave empty for public servers." +msgstr "Password richiesta per entrare. Lasciare vuoto per i server pubblici." + +#: forms.py:86 +msgid "If on, IP adresses of the clients are not logged." +msgstr "Se ц╗ acceso, indirizzi IP dei clienti non vengono registrati." + +#: forms.py:142 +#, python-format +msgid "" +"Port number %(portno)d is not within the allowed range %(minrange)d - %" +"(maxrange)d" +msgstr "" +"Il Port %(portno)d non ц╗ consentito nel range %(minrange)d - %(maxrange)d" + +#: forms.py:152 +#, fuzzy +msgid "Default config" +msgstr "Canale default" + +#: forms.py:165 templates/mumble/offline.html:12 +msgid "This server is currently offline." +msgstr "Al momento questo server ц╗ offline." + +#: forms.py:190 +msgid "That name is forbidden by the server." +msgstr "Questo nome ц╗ proibito dal server." + +#: forms.py:193 models.py:587 +msgid "Another player already registered that name." +msgstr "Un altro giocatore ц╗ stato giц═ sotto questo nome registrato." + +#: forms.py:201 forms.py:307 models.py:589 +msgid "Cannot register player without a password!" +msgstr "Non ц╗ possibile registrarsi senza una password!" + +#: forms.py:213 models.py:179 +msgid "Server Password" +msgstr "Server password" + +#: forms.py:214 +msgid "" +"This server is private and protected mode is active. Please enter the server " +"password." +msgstr "" +"Server privato, la protezione di registro ц╗ attiva. Digiti prego la password " +"d'accesso." + +#: forms.py:222 forms.py:274 +msgid "The password you entered is incorrect." +msgstr "La password d'accesso non ц╗ corretta." + +#: forms.py:237 +msgid "Link account" +msgstr "Link il conto" + +#: forms.py:238 +msgid "" +"The account already exists and belongs to me, just link it instead of " +"creating." +msgstr "L'account esiste giц═." + +#: forms.py:255 +msgid "No such user found." +msgstr "Nessun utente trovato." + +#: forms.py:290 +msgid "That account belongs to someone else." +msgstr "L'account appartiene a qualcun altro." + +#: forms.py:293 +msgid "Linking Admin accounts is not allowed." +msgstr "Collegare account Amministratore non ц╗ permesso." + +#: forms.py:322 templates/mumble/mumble.html:65 +#: templates/mumble/mumble.html.py:148 templates/mumble/mumble.html:278 +msgid "User Texture" +msgstr "Immagine del Utente" + +#: models.py:63 +msgid "DBus or ICE base" +msgstr "DBus- o ICE-base" + +#: models.py:64 +msgid "" +"Examples: 'net.sourceforge.mumble.murmur' for DBus or 'Meta:tcp -h 127.0.0.1 " +"-p 6502' for Ice." +msgstr "" +"Esempi: 'net.sourceforge.mumble.murmur' per DBus o 'Meta: tcp-h 127.0.0.1-p " +"6502' per ICE." + +#: models.py:65 +msgid "Ice Secret" +msgstr "ICE Secret" + +#: models.py:68 models.py:159 +msgid "Mumble Server" +msgstr "Mumble Server" + +#: models.py:69 +msgid "Mumble Servers" +msgstr "Mumble Servers" + +#: models.py:160 +msgid "Server Name" +msgstr "Nome del Server" + +#: models.py:161 +msgid "Server ID" +msgstr "Server-ID" + +#: models.py:163 +msgid "" +"Hostname or IP address to bind to. You should use a hostname here, because " +"it will appear on the global server list." +msgstr "" +"Hostname o l'indirizzo IP da associare. Si dovrebbe usare un hostname qui, " +"perchц╘ sarц═ visualizzato nella lista globale dei server." + +#: models.py:166 +msgid "Port number to bind to. Leave empty to auto assign one." +msgstr "Numero del Port da associare. Lasciare vuoto per auto-assegnare uno." + +#: models.py:167 +msgid "Server Display Address" +msgstr "l'indirizzo Server display" + +#: models.py:168 +msgid "" +"This field is only relevant if you are located behind a NAT, and names the " +"Hostname or IP address to use in the Channel Viewer and for the global " +"server list registration. If not given, the addr and port fields are used. " +"If display and bind ports are equal, you can omit it here." +msgstr "" +"Questo campo ц╗ rilevante solo se si trova dietro un NAT, nomi e il Hostname " +"o indirizzi IP da utilizzare nel canale Viewer e per la lista globale dei " +"Server. \n" +"\n" +"Se il Port del display e il Port del bind sono uguali, ц╗ possibile omettere " +"qui." + +#: models.py:174 +msgid "Superuser Password" +msgstr "SuperUser password" + +#: models.py:177 +msgid "Website URL" +msgstr "URL del sito web" + +#: models.py:178 +msgid "Welcome Message" +msgstr "Messaggio di benvenuto" + +#: models.py:180 +msgid "Max. Users" +msgstr "Max. Utenti" + +#: models.py:181 +msgid "Bandwidth [Bps]" +msgstr "larghezza di banda [Bps]" + +#: models.py:182 +msgid "SSL Certificate" +msgstr "SSL-Certificato" + +#: models.py:183 +msgid "SSL Key" +msgstr "SSL-Chiave" + +#: models.py:184 +msgid "Player name regex" +msgstr "Regex per giocatori" + +#: models.py:185 +msgid "Channel name regex" +msgstr "Regex per Canali" + +#: models.py:186 +msgid "Default channel" +msgstr "Canale default" + +#: models.py:187 +msgid "Timeout" +msgstr "Timeout" + +#: models.py:189 +msgid "IP Obfuscation" +msgstr "Anonimizza indirizzo IP" + +#: models.py:190 +msgid "Require Certificate" +msgstr "Serve il Certificate" + +#: models.py:191 +msgid "Maximum length of text messages" +msgstr "La lunghezza massima dei messaggi" + +#: models.py:192 +msgid "Allow HTML to be used in messages" +msgstr "Permettere HTML nei messaggi" + +#: models.py:193 +msgid "Publish this server via Bonjour" +msgstr "Pubblica questo server tramite Bonjour" + +#: models.py:194 +msgid "Boot Server when Murmur starts" +msgstr "Boot Server quando si avvia Murmur" + +#: models.py:212 models.py:213 +msgid "Boot Server" +msgstr "Boot Server" + +#: models.py:217 models.py:550 +msgid "Server instance" +msgstr "Istanza di server" + +#: models.py:218 +msgid "Server instances" +msgstr "Istanze di server" + +#: models.py:510 models.py:690 +msgid "This field must not be updated once the record has been saved." +msgstr "" +"Questo campo non si puo cambiare, dopo che ц╘ stato salvato per la prima " +"volta." + +#: models.py:547 +msgid "Mumble player_id" +msgstr "ID del giocatore in Murmur" + +#: models.py:548 +msgid "User name and Login" +msgstr "Nome Utente e Login" + +#: models.py:549 +msgid "Login password" +msgstr "Login password" + +#: models.py:551 templates/mumble/mumble.html:293 +msgid "Account owner" +msgstr "Proprietario del account" + +#: models.py:553 +msgid "The user's comment." +msgstr "Commento dell'utente." + +#: models.py:554 +msgid "The user's hash." +msgstr "Hash dell'utente." + +#: models.py:558 +msgid "User account" +msgstr "Account del utente" + +#: models.py:559 +msgid "User accounts" +msgstr "Accounts degli utenti" + +#: models.py:566 +#, python-format +msgid "Mumble user %(mu)s on %(srv)s owned by Django user %(du)s" +msgstr "Account %(mu)s su %(srv)s con Utente %(du)s" + +#: templates/mumble/list.html:20 +msgid "No server instances have been configured yet." +msgstr "Nessune istanze del server sono stati configurati." + +#: templates/mumble/mumble.html:16 +msgid "" +"\n" +" Hint:
\n" +" This area is used to display additional information for each channel " +"and player, but requires JavaScript to be\n" +" displayed correctly. You will not see the detail pages, but you can " +"use all links and forms\n" +" that are displayed.\n" +" " +msgstr "" +"\n" +" Nota
\n" +" Questo settore ц╗ utilizzato per visualizzare informazioni aggiuntive " +"per ogni canale e giocatore, ma richiede JavaScript per essere\n" +" visualizzato correttamente. Non vedi il dettaglio delle pagine, ma " +"puoi utilizzare tutti i link e le forme\n" +" che vengono visualizzati.\n" +" " + +#: templates/mumble/mumble.html:31 +msgid "Website" +msgstr "Sito web" + +#: templates/mumble/mumble.html:33 +msgid "Server version" +msgstr "Versione del Server" + +#: templates/mumble/mumble.html:34 +msgid "Minimal view" +msgstr "La visualizzazione minimale" + +#: templates/mumble/mumble.html:37 +msgid "Welcome message" +msgstr "Messaggio di benvenuto" + +#: templates/mumble/mumble.html:43 +msgid "Server registration" +msgstr "Registrazione Server" + +#: templates/mumble/mumble.html:46 +msgid "You are registered on this server" +msgstr "Sei registrato su questo Server " + +#: templates/mumble/mumble.html:48 +msgid "You do not have an account on this server" +msgstr "Non sei registrato su questo Server " + +#: templates/mumble/mumble.html:57 +#, python-format +msgid "" +"\n" +"

You need to be logged in to be able " +"to register an account on this Mumble server.

\n" +" " +msgstr "" +"\n" +"

Devi avere un login per registrarti " +"su questo mumble server

\n" +" " + +#: templates/mumble/mumble.html:67 +msgid "" +"\n" +" Sorry, due to a bug in Murmur 1.2.2, displaying and setting the " +"Texture is disabled.\n" +" " +msgstr "" +"\n" +"Scusa, a causa di un bug in Murmur 1.2.2, la visualizzazione e " +"l'impostazione ц╗ disattivato." + +#: templates/mumble/mumble.html:72 +msgid "" +"\n" +" You can upload an image that you would like to use as your user " +"texture here.\n" +" " +msgstr "" +"\n" +"ц┬ possibile caricare l'immagine che si desidera utilizzare come texture " +"utente." + +#: templates/mumble/mumble.html:77 +msgid "Your current texture is" +msgstr "Il tuo attuale immagine" + +#: templates/mumble/mumble.html:80 +msgid "You don't currently have a texture set" +msgstr "Al momento non hai dei texture set" + +#: templates/mumble/mumble.html:84 +msgid "" +"\n" +" Hint: The texture image needs to be 600x60 in size. If " +"you upload an image with\n" +" a different size, it will be resized accordingly.
\n" +" " +msgstr "" +"\n" +"Nota! La immagine deve avere le dimensioni di 600x60 pixel. Se hai " +"un'immagine con una dimensione diversa, si aggiusta automaticamente. " + +#: templates/mumble/mumble.html:103 +msgid "Server administration" +msgstr "Amministrazione del Server" + +#: templates/mumble/mumble.html:117 +msgid "Player" +msgstr "Giocatore" + +#: templates/mumble/mumble.html:119 +msgid "Online since" +msgstr "Online da" + +#: templates/mumble/mumble.html:120 templates/mumble/player.html:9 +msgid "Authenticated" +msgstr "Authenticated" + +#: templates/mumble/mumble.html:121 templates/mumble/mumble.html.py:136 +msgid "Admin" +msgstr "Amministratore" + +#: templates/mumble/mumble.html:122 templates/mumble/player.html:12 +msgid "Muted" +msgstr "Muto" + +#: templates/mumble/mumble.html:123 templates/mumble/player.html:18 +msgid "Deafened" +msgstr "Sordo" + +#: templates/mumble/mumble.html:124 templates/mumble/player.html:21 +msgid "Muted by self" +msgstr "Mutato solo" + +#: templates/mumble/mumble.html:125 templates/mumble/player.html:24 +msgid "Deafened by self" +msgstr "Rintronarsi" + +#: templates/mumble/mumble.html:127 +msgid "IP Address" +msgstr "Indirizzo IP del Server" + +#: templates/mumble/mumble.html:131 +msgid "User" +msgstr "Utente" + +#: templates/mumble/mumble.html:134 +msgid "Full Name" +msgstr "Nome" + +#: templates/mumble/mumble.html:137 +msgid "Sign-up date" +msgstr "Data di registrazione" + +#: templates/mumble/mumble.html:142 +msgid "User Comment" +msgstr "User Comment" + +#: templates/mumble/mumble.html:154 templates/mumble/mumble.html.py:168 +msgid "Kick user" +msgstr "Kick utente" + +#: templates/mumble/mumble.html:160 +msgid "Reason" +msgstr "Diritto" + +#: templates/mumble/mumble.html:165 +msgid "Ban user" +msgstr "Ban utenti" + +#: templates/mumble/mumble.html:175 +msgid "Channel" +msgstr "Canale" + +#: templates/mumble/mumble.html:177 +msgid "Channel ID" +msgstr "ID del Canale" + +#: templates/mumble/mumble.html:179 +msgid "Connect" +msgstr "Connect" + +#: templates/mumble/mumble.html:182 +msgid "Channel description" +msgstr "descrizione del canale" + +#: templates/mumble/mumble.html:229 +msgid "Delete" +msgstr "Cancellare" + +#: templates/mumble/mumble.html:265 +msgid "Server Info" +msgstr "Informazioni del Server" + +#: templates/mumble/mumble.html:266 +msgid "Registration" +msgstr "Registrazione" + +#: templates/mumble/mumble.html:275 +msgid "Administration" +msgstr "Amministrazione" + +#: templates/mumble/mumble.html:282 +msgid "User List" +msgstr "Lista dei utenti" + +#: templates/mumble/mumble.html:286 +msgid "name" +msgstr "Nome" + +#: templates/mumble/mumble.html:306 +msgid "Change password" +msgstr "Cambia la password" + +#: templates/mumble/mumble.html:319 +msgid "Add" +msgstr "Aggiungere" + +#: templates/mumble/mumble.html:331 +msgid "Save" +msgstr "Salva" + +#: templates/mumble/mumble.html:357 +msgid "Resync with Murmur" +msgstr "Sincronizza con Murmur" + +#: templates/mumble/player.html:15 +msgid "Suppressed" +msgstr "Soppresso" + +#: templates/mumble/player.html:27 +msgid "has a User Comment set" +msgstr "Ha un commento del Utente set" diff --git a/mumble/locale/ja/LC_MESSAGES/django.mo b/mumble/locale/ja/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..9e4efda9f916575e9eb8711160fc8448c26a059f GIT binary patch literal 10072 zcmbuDdvILUea8;ZLk*E36BWl!7qY?gjE zsR8f+_?fGey5={P+C=-tHl;oZJ_pJcqu@2*aqxG+SHa%~e*}IRd=H!p{sNSBw_K+b zTdMisr@(tb{;7xf_#*fycpEqb%J^Xrk<@9BEz~(s_IVflEclP$^QdW&QtxUjnaTQ0CtX(xv8uGXH))MBbI)*TFvl#lFvjGVc)h zF>oAg2Ty|{$G?KI-v<^&*-qM@1VzsWL6Ku6xEgf9yTP+|{69dE>jT^VEJBDJU*JRb zy&aTu?g7Q#D?p~I$3U@r0Dc@?4)*BQpMb)jf3yAn z4f0Q2i_miJjiBs%J1FvWf+EK^K(Xr|fwzL5#hu_v+NVMB$5l+0eP)BQ?*i~fc-;jq zr0p>%d_4j_0{#dTy>9v)qt~sVoO>H6`rK{Xi!442ihsTX$~itL>wf@>-djM?_eqO~ zL5ZK!pq%?UI0yVGsO7~NH`1OBiXHC<#lDY%cOv(9!ExG8F}NID#N>W(CwMjEAAq}Q z|Kf9SAKdbJ!-K=1$Ug>3{~6o=WAJ9$?}4|2{{xD?w|v3ad7j0kpy=@^DE#XOC2sfH z{<9X}1u?z)35bbR4Mb%1OK=wWDGnE2-VKVq7J_o#YEb4q2Fkugi_d^E|26PY@Vsqb zPcqv>`}3gi<2Z;3)j3dj@ndiv_&&%Ib=^&7|5+fWQTKt%z(+vQ>wq0U35wi*1&W^U zfg<142!(0Yr!CI5ct0q9_!cPhUIImr2~h0)*WfMS--F`s{{m(I8wfg)YYvzJmw~eW z3@Gz1f`1PFI~ahjW9;w`;B4CeWZPFUS>*o&D0ccBDCd3&lym0T_5x7$f55iafFkGj zEQX-)bvr2g?FD82QBdr429*8&){g%S6#0J+qLONZM6zxUDD%G#iX5wL`@43$2zJxI z1zZgNGgty|`U9i)K~UuSOHlUtDR>_IPf+}M0%M9_{sr6yUPthYTu*~?-Y6*RUIj&- zzp?nf?VkZf?vD{{qVE?$S$`WS>+ZAoh{XWBmi}R|0~`V6{J#K&&p!v{oEvD|27U#+ z7W_jQY;&6aJm_Z&olf_F=M=mRg<{@w{6V+ha~AY0%z9orTyRIvLZ=WoJ)Uz{N5_V{ zIvhXig#Mp-DQ9_~voR<-#at$E(~g_9E7$vlftEvDC++w3d3i5e&}-*9e!*GqXEL&F z-jnUvptm>3r~PbyW@BnWPu{s>Vbd9k+) zgRGlDqP`$M=t|4so8!5?15WRNo6UL|Es}`m<>w-P0bBoR(Z}Gf@M6^wJO z3GDtHhA;A7ZvkI8Vr%WJY=HYbtbuZAui*L_CkIzTv$t?TyF_e_YcqcKaWudvB4B7w z&<;YaSe3d>5{u*(%89~0OS}cHGm!WC?z{Qc*_oi<&puYnXXb=AFQn1WBtN_04mT#L zm|2$Py?#F|;Au!A%ql`XEL0Gyb`=L@7Cad8@~H)PETch?Flwe}f&8T7(XtfUrUKltVthhQU|o8Fj6D(VV* zv+kgmQj3br%@+J#x8SAKVxcru`I2Q$j~jYwsps~2Srm=GI6WKjPo|HKKDU^uTRmw9**?FY z;fz=C)UplkU@qf@ozASRS4f4#d9MfU`n}Ykb~YeUJ4=U1eEh5~NZeWI%{lD@&b;|| zr#kpM&uPy&ciq`B-^^X!i!&Z5!smz4Lp>OT1+9=sze?heXbQE*eVASdOt^%Yl=Oqk zuR5T`&lm-F1bA*bi`XM!H0+HjK?3JWR4L6A`nlCLX!`idd65E~;M z-tWzO1w}RvvU814GL;|?`gExFW_*~WlZhh?Fk!>CS(uS!2SOrW%x1}3>LI`Pal^C~ zgl`-ZbUehpp)8aQiS}SUfvi?Ybk=Fp(x}`>P$NuA)lUIBnj*D z`^Y?4W@VOiNXqfCcifQiP_432M~LX$tdrilz|W?f6`q{o4dx0PHE(mpo{ZmXMF~@C zmCo!Ibl$LWwkpVzM=*;vK#o>6m_wp8?vjYdO_If}=bXHbcWmWn^;aghi|O1Awy7Go zlyV?ud!vl>yG8j3L%%`fXTn;P2$C~uCqAEt|M60YR?Vf^J<&5F{QDN z>98qwwXu-1#LE}_KAjp_5pK-(azDW#b2F7pUZijJ)Nzhhhy9jfI67OrUYkM*0 zq`4K;nqtn&Ym(KaDCTlH_|vLe?vnWC>+v!{wqNI|Fc|dkjN@fO+@ojdga$cCm_2?v z4W)?=p+{(LNM=D5dpX9}mbU@b%>CZ5`ydH!T`?rdNctefiF#af65pgLvR1ZVPOV=q zx>_SYksCEbi4aqIm}#QltdJNR9Ar{c-+&tqNQX%7d!$>w1lf$-s2Oj_<-+$(g(~Gn zCeSx25exSzHl2qTb!_2NW)9)tX69-GO5qg@ioFYtu4xQQIpGfeki;(%P~~ywlg>hu%|fDtgr&`D{4aLxEm96%-4tL9Jy(e>h8w?@eO` z<}}}Id2Je&olT94`A%m+HsGo(;QR3Y{y|<3DU80bt!sJLvbwDEQXOrSi88Cbdt(j( zq%63D%iYi3E46nXRqtEd{XqLY^?7m#&&&2@rwegR6ZY-pPGJSOH>+;N(ZCzj_Sx0GPArr zDjknXo1N;Rv(-I}O+=eYQRR4jS!vhw;ayRABr0=&-I~XE({ot0UAl0gwiy}6qsnMh zc`GWvVWloV6_uXW|8{cN)cy-IyUvJw<(;~7Ov}AH*;%#eXRM=@oM_#wG(LTJZ0g8) zSyb5*m0yiYFGr>zCnW~PPFF`JjD?g5k?Nk^)$LGlqV_BCOCUH7}0cQ?zzBssbn06C0TieT*w)WKazS`O~ef(tYxhGnC zv5z?8)XUSOBSPkpcZ`;*_QcVtH=fsz=ctT2@Ke$hk7zYrI$IstH#J(Soq9=4Z#!Nc z-iq8!y-{UbRM{Jqj}b@W@5|(G9xg)){;}X?D5%Rz z^E|Z^Kl-Ju3@21|_}J7l}ZX+7t$~e;7;m zR$a8(flbvTuOtE*Zf}jsq%Uza9w(6;8W%ktMc=c6H?<42+8A7&wnwnN= z9YH@7nSZ_h_!j{5PJRNw9;O&dG_XGaI5yg+_Me{~+teCQO}#o-k*xZp7j5n2Vcat{vAOyJmCLRxaCN`9^@=}M zG$ny%S%{_Du5H>)#ycd0QtiO;xO3c8W*A93SO*8iQ@jbwh^2n%qrEG(E}zgkb9~n5*SXTPHQ-6qB(>A#QxtH_8Pslaww( zHHbW3J%akYEeLH2uLHGVibk1#X_AbzR~S#N-r{3|5Rs`zOl~wY;$$Xe0yQ6>i-K?jS2as<+K+wBBwZwT}CQEmzu9Cna8+MEE{2lE{ZxyjYsMAd~!vQ@3ODE94^vpkrw> z;~ySzF)RKbA$o+Bep&nV} JGJ$OQ{C^qANy`8L literal 0 HcmV?d00001 diff --git a/mumble/locale/ja/LC_MESSAGES/django.po b/mumble/locale/ja/LC_MESSAGES/django.po new file mode 100644 index 0000000..9035f9d --- /dev/null +++ b/mumble/locale/ja/LC_MESSAGES/django.po @@ -0,0 +1,543 @@ +# Japanese translation file for Mumble-Django. +# +# Copyright (C) 2009, withgod +# Michael "Svedrin" Ziegler +# +# Mumble-Django is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This package is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +msgid "" +msgstr "" +"Project-Id-Version: Mumble-Django v0.8\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-03-14 18:23+0100\n" +"PO-Revision-Date: 2010-03-15 21:47\n" +"Last-Translator: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Translated-Using: django-rosetta 0.5.2\n" + +#: admin.py:34 admin.py:51 +msgid "Master is running" +msgstr "Ц┐·Ц┌╧Ц┌©Ц┐╪Е╝÷Х║▄Д╦╜" + +#: admin.py:59 models.py:162 templates/mumble/mumble.html:28 +msgid "Server Address" +msgstr "Ц┌╣Ц┐╪Ц┐░Ц┌╒Ц┐┴Ц┐╛Ц┌╧" + +#: admin.py:66 models.py:165 +msgid "Server Port" +msgstr "Ц┌╣Ц┐╪Ц┐░Ц┐²Ц┐╪Ц┐┬" + +#: admin.py:71 +msgid "Instance is running" +msgstr "Ц┌╓Ц┐ЁЦ┌╧Ц┌©Ц┐ЁЦ┌╧Е╝÷Х║▄Д╦╜" + +#: admin.py:81 +msgid "Registered users" +msgstr "Ц┐╕Ц┐╪Ц┌╤Г≥╩И▄╡" + +#: admin.py:91 +msgid "Online users" +msgstr "Ц┌╙Ц┐ЁЦ┐╘Ц┌╓Ц┐ЁЦ│╝Ц┐╕Ц┐╪Ц┌╤" + +#: admin.py:101 +msgid "Channel count" +msgstr "Ц┐│Ц┐ёЦ┐ЁЦ┐█Ц┐╚Ф∙╟" + +#: admin.py:108 +msgid "Yes" +msgstr "Ц│╞Ц│└" + +#: admin.py:110 +msgid "No" +msgstr "Ц│└Ц│└Ц│┬" + +#: admin.py:114 +msgid "Public" +msgstr "Е┘╛И√▀" + +#: admin.py:132 models.py:631 templates/mumble/mumble.html:223 +msgid "Admin on root channel" +msgstr "Г╝║Г░├Х─┘Ф╗╘И≥░Ц┌▓Д╦▌Ц│┬Ц┌▀" + +#: forms.py:83 +msgid "Password required to join. Leave empty for public servers." +msgstr "Ц┌╣Ц┐╪Ц┐░Ц│╚Е▐┌Е┼═Ц│≥Ц┌▀Ц│╚Ц│╞Ц┐▒Ц┌╧Ц┐╞Ц┐╪Ц┐┴Ц│▄Е©┘Х╕│Ц─┌Г╘╨Г≥╫Ц│╚Ц│≥Ц┌▀Ц│╗Е┘╛И√▀Ц┌╣Ц┐╪Ц┐░Ц│╚Ц│╙Ц┌┼Ц│╬Ц│≥Ц─┌" + +#: forms.py:86 +msgid "If on, IP adresses of the clients are not logged." +msgstr "If on, IP adresses of the clients are not logged." + +#: forms.py:142 +#, python-format +msgid "Port number %(portno)d is not within the allowed range %(minrange)d - %(maxrange)d" +msgstr "Ц┐²Ц┐╪Ц┐┬Г∙╙Е▐╥ %(portno)d Ц│╞Х╗╠Е▐╞Ц│∙Ц┌▄Ц│╕Ц│└Ц│╬Ц│⌡Ц┌⌠Ц─│Ф╛║Ц│╝Г╞└Е⌡╡Ц│▄Х╗╠Е▐╞Ц│∙Ц┌▄Ц│╕Ц│└Ц│╬Ц│≥ %(minrange)d - %(maxrange)d" + +#: forms.py:152 +msgid "Default config" +msgstr "Ц┐┤Ц┐∙Ц┌╘Ц┐╚Ц┐┬Ц┌ЁЦ┐ЁЦ┐∙Ц┌ёЦ┌╟" + +#: forms.py:165 templates/mumble/offline.html:12 +msgid "This server is currently offline." +msgstr "Ц│⌠Ц│╝Ц┌╣Ц┐╪Ц┐░Ц┐╪Ц│╞Г▐╬Е°╗Е│°Ф╜╒Д╦╜Ц│╖Ц│≥Ц─┌" + +#: forms.py:190 +msgid "That name is forbidden by the server." +msgstr "Ц│²Ц│╝Е░█Е┴█Ц│╞Ц┌╣Ц┐╪Ц┐░Ц│╚Ц┌┬Ц│ёЦ│╕Д╦█Е▐╞Х╕√Ц│╚Ц│∙Ц┌▄Ц│╕Ц│└Ц│╬Ц│≥" + +#: forms.py:193 models.py:587 +msgid "Another player already registered that name." +msgstr "Ц│²Ц│╝Е░█Е┴█Ц│╞Ф≈╒Ц│╚Д╫©Ц┌▐Ц┌▄Ц│╕Ц│└Ц│╬Ц│≥." + +#: forms.py:201 forms.py:307 models.py:589 +msgid "Cannot register player without a password!" +msgstr "Ц┐▒Ц┌╧Ц┐╞Ц┐╪Ц┐┴Ц│╞Е©┘И═┬Ц│╖Ц│≥" + +#: forms.py:213 models.py:179 +msgid "Server Password" +msgstr "Ц┌╣Ц┐╪Ц┐░Ц┐▒Ц┌╧Ц┐╞Ц┐╪Ц┐┴" + +#: forms.py:214 +msgid "This server is private and protected mode is active. Please enter the server password." +msgstr "Ф°╛Ц┌╣Ц┐╪Ц┐░Ц┌▓Ц┐≈Ц┐╘Ц┌╓Ц┐≥Ц┐╪Ц┐┬Е▐┼Ц│ЁЦ┐≈Ц┐╜Ц┐├Ц┌╞Ц┐┬Ц┐╒Ц┐╪Ц┐┴Ц│╖Е┬╘Г■╗Ц│≥Ц┌▀Е═╢Е░┬Ц│╞Ц─│Ц┐▒Ц┌╧Ц┐╞Ц┐╪Ц┐┴Ц┌▓Е┘╔Е┼⌡Ц│≈Ц│╕Ц│▐Ц│═Ц│∙Ц│└Ц─┌" + +#: forms.py:222 forms.py:274 +msgid "The password you entered is incorrect." +msgstr "Ц┐▒Ц┌╧Ц┐╞Ц┐╪Ц┐┴Ц│▄Д╦█Ф╜ёЦ│╖Ц│≥Ц─┌" + +#: forms.py:237 +msgid "Link account" +msgstr "Ц┌╒Ц┌╚Ц┌╕Ц┐ЁЦ┐┬Ц┐╙Ц┐ЁЦ┌╞" + +#: forms.py:238 +msgid "The account already exists and belongs to me, just link it instead of creating." +msgstr "Ц┌╒Ц┌╚Ц┌╕Ц┐ЁЦ┐┬Ц│╞Ф≈╒Ц│╚Е╜≤Е°╗Ц│≈Ц│╕Ц│└Ц│╕Ц─│Х┤╙Е┬├Ц│╚Ц┐╙Ц┐ЁЦ┌╞Ц│≈Ц│╕Ц│└Ц│╬Ц│≥Ц─┌Ц┐╙Ц┐ЁЦ┌╞Ц│╝Е╓┴Ц┌▐Ц┌┼Ц│╚Д╫°Ф┬░Ц│≈Ц│╕Д╦▀Ц│∙Ц│└Ц─┌" + +#: forms.py:255 +msgid "No such user found." +msgstr "Ц┐╕Ц┐╪Ц┌╤Ц│▄Х╕▀Ц│╓Ц│▀Ц┌┼Ц│╬Ц│⌡Ц┌⌠" + +#: forms.py:290 +msgid "That account belongs to someone else." +msgstr "Ц│⌠Ц│╝Ц┌╒Ц┌╚Ц┌╕Ц┐ЁЦ┐┬Ц│╞Ф≈╒Ц│╚Д╩√Ц│╝Ц┌╒Ц┌╚Ц┌╕Ц┐ЁЦ┐┬Ц│╚Ц┐╙Ц┐ЁЦ┌╞Ц│∙Ц┌▄Ц│╕Ц│└Ц│╬Ц│≥Ц─┌" + +#: forms.py:293 +msgid "Linking Admin accounts is not allowed." +msgstr "Г╝║Г░├Х─┘Ц┌╒Ц┌╚Ц┌╕Ц┐ЁЦ┐┬Ц│╦Ц│╝Ц┐╙Ц┐ЁЦ┌╞Ц│╞Х╗╠Е▐╞Ц│∙Ц┌▄Ц│╕Ц│└Ц│╬Ц│⌡Ц┌⌠" + +#: forms.py:322 templates/mumble/mumble.html:65 +#: templates/mumble/mumble.html.py:148 templates/mumble/mumble.html:278 +msgid "User Texture" +msgstr "Ц┐╕Ц┐╪Ц┌╤Г■╩Е┐▐" + +#: models.py:63 +msgid "DBus or ICE base" +msgstr "DBusЦ┌┌Ц│≈Ц│▐Ц│╞ICE" + +#: models.py:64 +msgid "Examples: 'net.sourceforge.mumble.murmur' for DBus or 'Meta:tcp -h 127.0.0.1 -p 6502' for Ice." +msgstr "Д╬▀: DBusЦ│╙Ц┌┴'net.sourceforge.mumble.murmur' IceЦ│╙Ц┌┴'Meta:tcp -h 127.0.0.1 -p 6502'" + +#: models.py:65 +msgid "Ice Secret" +msgstr "Ice Secret" + +#: models.py:68 models.py:159 +msgid "Mumble Server" +msgstr "Ц┐·Ц┐ЁЦ┐√Ц┐╚Ц┐╕Ц┐╪Ц┌╤ID" + +#: models.py:69 +msgid "Mumble Servers" +msgstr "Ц┐·Ц┐ЁЦ┐√Ц┐╚Ц┐╕Ц┐╪Ц┌╤ID" + +#: models.py:160 +msgid "Server Name" +msgstr "Ц┌╣Ц┐╪Ц┐░Е░█" + +#: models.py:161 +msgid "Server ID" +msgstr "Ц┌╣Ц┐╪Ц┐░ID" + +#: models.py:163 +msgid "Hostname or IP address to bind to. You should use a hostname here, because it will appear on the global server list." +msgstr "Е┬╘Г■╗Ц│≥Ц┌▀Ц┐⌡Ц┌╧Ц┐┬Е░█Ц┌┌Ц│≈Ц│▐Ц│╞IPЦ┌╒Ц┐┴Ц┐╛Ц┌╧. Ц┐⌡Ц┌╧Ц┐┬Е░█Ц┌┌Е┬╘Г■╗Ц│≥Ц┌▀Ц│⌠Ц│╗Ц│▄Е┤╨Ф²╔Ц│╬Ц│≥Ц─┌ Ц┌╣Ц┐╪Ц┐░Д╦─Х╕╖Ц│╚Х║╗Г╓╨Ц│∙Ц┌▄Ц│╬Ц│≥Ц─┌." + +#: models.py:166 +msgid "Port number to bind to. Leave empty to auto assign one." +msgstr "Е┬╘Г■╗Ц│≥Ц┌▀Ц┐²Ц┐╪Ц┐┬Г∙╙Е▐╥Ц┌▓Ф▄┤Е╝ Ц│≈Ц│╬Ц│≥Ц─│Ф°╙Ф▄┤Е╝ Ц│╝Е═╢Е░┬Г╘╨Ц│└Ц│╕Ц│└Ц┌▀Ц┐²Ц┐╪Ц┐┬Ц┌▓Е┬╘Г■╗Ц│≈Ц│╬Ц│≥Ц─┌" + +#: models.py:167 +msgid "Server Display Address" +msgstr "Ц┌╣Ц┐╪Ц┐░Ц┌╒Ц┐┴Ц┐╛Ц┌╧" + +#: models.py:168 +msgid "This field is only relevant if you are located behind a NAT, and names the Hostname or IP address to use in the Channel Viewer and for the global server list registration. If not given, the addr and port fields are used. If display and bind ports are equal, you can omit it here." +msgstr "Ф°╛И═┘Г⌡╝Ц│╞Ц─│NATЦ│╝Е├┘Е│╢Г╜┴Ц│╚Ц│┌Ц┌▀Ц┌╣Ц┐╪Ц┐░Ц│╚И│╘Е┬┤Ц│╖Ц│≥Ц─┌Ц┐⌡Ц┌╧Ц┐┬Е░█Ц┌┌Ц│≈Ц│▐Ц│╞IPЦ┌╒Ц┐┴Ц┐╛Ц┌╧Ц┌▓Ц─│Ц┐│Ц┐ёЦ┐ЁЦ┐█Ц┐╚Ц┐⌠Ц┐╔Ц┐╪Ц┌╒Ц┌└Ц─│Ц┌╣Ц┐╪Ц┐░Ц┐╙Ц┌╧Ц┐┬Г≥╩И▄╡Ц│╚Д╫©Ц│└Ц│╬Ц│≥Ц─┌ " + +#: models.py:174 +msgid "Superuser Password" +msgstr "Г╝║Г░├Х─┘Ц┐▒Ц┌╧Ц┐╞Ц┐╪Ц┐┴" + +#: models.py:177 +msgid "Website URL" +msgstr "Ц┌╕Ц┌╖Ц┐√Ц┌╣Ц┌╓Ц┐┬Ц│╝URL" + +#: models.py:178 +msgid "Welcome Message" +msgstr "Ц┌╣Ц┐╪Ц┐░Ц┐║Ц┐┐Ц┌╩Ц┐╪Ц┌╦" + +#: models.py:180 +msgid "Max. Users" +msgstr "Ф°─Е╓╖Ц┐╕Ц┐╪Ц┌╤Ф∙╟" + +#: models.py:181 +msgid "Bandwidth [Bps]" +msgstr "Е╦╞Е÷÷Е┬╤И≥░ [Bps]" + +#: models.py:182 +msgid "SSL Certificate" +msgstr "SSLХ╙█Х╗╪" + +#: models.py:183 +msgid "SSL Key" +msgstr "SSLХ╙█Х╗╪И█╣" + +#: models.py:184 +msgid "Player name regex" +msgstr "Ц┐╕Ц┐╪Ц┌╤Е░█Ц│╝Ф╜ёХ╕▐Х║╗Г▐╬Е┬╤И≥░" + +#: models.py:185 +msgid "Channel name regex" +msgstr "Ц┐│Ц┐ёЦ┐ЁЦ┐█Ц┐╚Е░█Ц│╝Ф╜ёХ╕▐Х║╗Г▐╬Е┬╤И≥░" + +#: models.py:186 +msgid "Default channel" +msgstr "Ц┐┤Ц┐∙Ц┌╘Ц┐╚Ц┐┬Ц┐│Ц┐ёЦ┐ЁЦ┐█Ц┐╚" + +#: models.py:187 +msgid "Timeout" +msgstr "Ц┌©Ц┌╓Ц┐═Ц┌╒Ц┌╕Ц┐┬" + +#: models.py:189 +msgid "IP Obfuscation" +msgstr "IPЦ┌╒Ц┐┴Ц┐╛Ц┌╧И ═Х■╫" + +#: models.py:190 +msgid "Require Certificate" +msgstr "SSLХ╙█Х╗╪" + +#: models.py:191 +msgid "Maximum length of text messages" +msgstr "Ц┐├Ц┌╜Ц┌╧Ц┐┬Ц┐║Ц┐┐Ц┌╩Ц┐╪Ц┌╦Ц│╝Ф°─Е╓╖Ф√┤Е╜≈Ф∙╟" + +#: models.py:192 +msgid "Allow HTML to be used in messages" +msgstr "HTMLЦ┐║Ц┐┐Ц┌╩Ц┐╪Ц┌╦Ц┌▓Х╗╠Е▐╞Ц│≥Ц┌▀" + +#: models.py:193 +msgid "Publish this server via Bonjour" +msgstr "BonjourЦ│╖Ц│⌠Ц│╝Ц┌╣Ц┐╪Ц┐░Ц┌▓Е┘╛И√▀Ц│≥Ц┌▀" + +#: models.py:194 +msgid "Boot Server when Murmur starts" +msgstr "MurmurХ╣╥Е▀∙Ф≥┌Ц│╚Ц┌╣Ц┐╪Ц┐░Ц┌▓Х╣╥Е▀∙Ц│≈Ц│╬Ц│≥" + +#: models.py:212 models.py:213 +msgid "Boot Server" +msgstr "Ц┌╣Ц┐╪Ц┐░Х╣╥Е▀∙" + +#: models.py:217 models.py:550 +msgid "Server instance" +msgstr "Ц┌╣Ц┐╪Ц┐░Ц┌╓Ц┐ЁЦ┌╧Ц┌©Ц┐ЁЦ┌╧" + +#: models.py:218 +msgid "Server instances" +msgstr "Е┘╗Ц┌╣Ц┐╪Ц┐░Ц┌╓Ц┐ЁЦ┌╧Ц┌©Ц┐ЁЦ┌╧" + +#: models.py:510 models.py:690 +msgid "This field must not be updated once the record has been saved." +msgstr "Ц│⌠Ц│╝И═┘Г⌡╝Ц│╞Ц┌╒Ц┐┐Ц┐≈Ц┐┤Ц┐╪Ц┐┬Е┤╨Ф²╔Ц│╬Ц│⌡Ц┌⌠." + +#: models.py:547 +msgid "Mumble player_id" +msgstr "Ц┐·Ц┐ЁЦ┐√Ц┐╚Ц┐╕Ц┐╪Ц┌╤ID" + +#: models.py:548 +msgid "User name and Login" +msgstr "Ц┐╕Ц┐╪Ц┌╤ID" + +#: models.py:549 +msgid "Login password" +msgstr "Ц┐▒Ц┌╧Ц┐╞Ц┐╪Ц┐┴" + +#: models.py:551 templates/mumble/mumble.html:293 +msgid "Account owner" +msgstr "Ц┌╒Ц┌╚Ц┌╕Ц┐ЁЦ┐┬Ц│╝Ф┴─Ф°┴Х─┘" + +#: models.py:553 +msgid "The user's comment." +msgstr "Ц┐╕Ц┐╪Ц┌╤Ц┌ЁЦ┐║Ц┐ЁЦ┐┬" + +#: models.py:554 +msgid "The user's hash." +msgstr "Ц┐╕Ц┐╪Ц┌╤Ц│╝Ц┐▐Ц┐┐Ц┌╥Ц┐╔" + +#: models.py:558 +msgid "User account" +msgstr "Ц┐╕Ц┐╪Ц┌╤Ц┌╒Ц┌╚Ц┌╕Ц┐ЁЦ┐┬" + +#: models.py:559 +msgid "User accounts" +msgstr "Е┘╗Ц┐╕Ц┐╪Ц┌╤Ц┌╒Ц┌╚Ц┌╕Ц┐ЁЦ┐┬" + +#: models.py:566 +#, python-format +msgid "Mumble user %(mu)s on %(srv)s owned by Django user %(du)s" +msgstr "Ц┐·Ц┐ЁЦ┐√Ц┐╚Ц┐╕Ц┐╪Ц┌╤ %(mu)s Ц│╞ %(srv)s Ц│╝DjangoЦ┐╕Ц┐╪Ц┌╤ %(du)s Ц│╖Ц│≥" + +#: templates/mumble/list.html:20 +msgid "No server instances have been configured yet." +msgstr "Ц┌╣Ц┐╪Ц┐░Х╗╜Е╝ Ц│▄Е╜≤Е°╗Ц│≈Ц│╬Ц│⌡Ц┌⌠" + +#: templates/mumble/mumble.html:16 +msgid "" +"\n" +" Hint:
\n" +" This area is used to display additional information for each channel and player, but requires JavaScript to be\n" +" displayed correctly. You will not see the detail pages, but you can use all links and forms\n" +" that are displayed.\n" +" " +msgstr "" +"\n" +" Ц┐▓Ц┐ЁЦ┐┬:
\n" +" Ц│⌠Ц│╝Ц┌╗Ц┐╙Ц┌╒Ц│╞Ц┐│Ц┐ёЦ┐ЁЦ┐█Ц┐╚Ф┐┘Е═╠Ц┌▓Ц┌└Ц┐≈Ц┐╛Ц┐╪Ц┐╓Ц┐╪Ф┐┘Е═╠Ц┌▓Х║╗Г╓╨Ц│≥Ц┌▀Ц│╝Ц│╚javascriptЦ│▄Е©┘Х╕│Ц│╖Ц│≥\n" +" javascriptЦ┌▓Ц┌╙Ц┐ЁЦ│╚Ц│≈Ц│╙Ц│└Ц│╗Ц─│Ц│≥Ц│╧Ц│╕Ц│╝Ф┐┘Е═╠Ц┌▓И√╡Х╕╖Ц│≥Ц┌▀Ц│⌠Ц│╗Ц│╞Е┤╨Ф²╔Ц│╬Ц│⌡Ц┌⌠Ц─┌ " + +#: templates/mumble/mumble.html:31 +msgid "Website" +msgstr "Ц┌╕Ц┌╖Ц┐√Ц┌╣Ц┌╓Ц┐┬" + +#: templates/mumble/mumble.html:33 +msgid "Server version" +msgstr "Ц┌╣Ц┐╪Ц┐░Ц┐░Ц┐╪Ц┌╦Ц┐╖Ц┐Ё" + +#: templates/mumble/mumble.html:34 +msgid "Minimal view" +msgstr "Г╦╝Е╟▐Х║╗Г╓╨" + +#: templates/mumble/mumble.html:37 +msgid "Welcome message" +msgstr "Ц┌╣Ц┐╪Ц┐░Ц┐║Ц┐┐Ц┌╩Ц┐╪Ц┌╦" + +#: templates/mumble/mumble.html:43 +msgid "Server registration" +msgstr "Ц┌╣Ц┐╪Ц┐░Г≥╩И▄╡" + +#: templates/mumble/mumble.html:46 +msgid "You are registered on this server" +msgstr "Ц┌╒Ц┌╚Ц┌╕Ц┐ЁЦ┐┬Ц│╞Ф≈╒Ц│╚Г≥╩И▄╡Ц│∙Ц┌▄Ц│╕Ц│└Ц│╬Ц│≥" + +#: templates/mumble/mumble.html:48 +msgid "You do not have an account on this server" +msgstr "Ц┌╒Ц┌╚Ц┌╕Ц┐ЁЦ┐┬Ц│╞Г≥╩И▄╡Ц│∙Ц┌▄Ц│╕Ц│└Ц│╬Ц│⌡Ц┌⌠" + +#: templates/mumble/mumble.html:57 +#, python-format +msgid "" +"\n" +"

You need to be logged in to be able to register an account on this Mumble server.

\n" +" " +msgstr "" +"\n" +"

Ц┐╜Ц┌╟Ц┌╓Ц┐ЁЦ│≈Ц│╕Ц│▀Ц┌┴ Ц┐╕Ц┐╪Ц┌╤Ц┌▓Ц│⌠Ц│╝MumbleЦ┌╣Ц┐╪Ц┐░Ц│╚Г≥╩И▄╡Ц│≥Ц┌▀Е©┘Х╕│Ц│▄Ц│┌Ц┌┼Ц│╬Ц│≥

\n" +" " + +#: templates/mumble/mumble.html:67 +msgid "" +"\n" +" Sorry, due to a bug in Murmur 1.2.2, displaying and setting the Texture is disabled.\n" +" " +msgstr "" +"\n" +"1.2.2 Ц│╖Ц│╞Ц┐╕Ц┐╪Ц┌╤Г■╩Е┐▐Ц│╝Х║╗Г╓╨Ц│▄Ц┐░Ц┌╟Ц│ёЦ│╕Ц┌▀Ц│÷Ц┌│Ц─│Ц┐╕Ц┐╪Ц┌╤Г■╩Е┐▐Х║╗Г╓╨Ц│╞Г└║Е┼╧Ц│╖Ц│≥Ц─┌" + +#: templates/mumble/mumble.html:72 +msgid "" +"\n" +" You can upload an image that you would like to use as your user texture here.\n" +" " +msgstr "" +"\n" +"Д╩╩Ф└▐Ц│╝Ц┐╕Ц┐╪Ц┌╤Г■╩Е┐▐Ц┌▓Ц│⌠Ц│⌠Ц│▀Ц┌┴Г≥╩И▄╡Ц│≥Ц┌▀Ц│⌠Ц│╗Ц│▄Ц│█Ц│╬Ц│≥Ц─┌" + +#: templates/mumble/mumble.html:77 +msgid "Your current texture is" +msgstr "Г▐╬Е°╗Ц│╝Г■╩Е┐▐" + +#: templates/mumble/mumble.html:80 +msgid "You don't currently have a texture set" +msgstr "Х╡╢Ф√╧Ц│╞Г■╩Е┐▐Ц┌▓Ф▄│Ц│ёЦ│╕Ц│└Ц│╬Ц│⌡Ц┌⌠" + +#: templates/mumble/mumble.html:84 +msgid "" +"\n" +" Hint: The texture image needs to be 600x60 in size. If you upload an image with\n" +" a different size, it will be resized accordingly.
\n" +" " +msgstr "" +"\n" +"Ц┐▓Ц┐ЁЦ┐┬: Г■╩Е┐▐Ц│╞600x60Ц│╖Ц│┌Ц┌▀Е©┘Х╕│Ц│▄Ц│┌Ц┌┼Ц│╬Ц│≥Ц─┌Ц│²Ц│╝Ц┌╣Ц┌╓Ц┌╨Ц┌▓Х╤┘Ц│┬Ц│÷Ц┌┼Е▐▌Ц│╬Ц┌┴Ц│╙Ц│└ Е═╢Е░┬Ц│╞Ц─│Ц┐╙Ц┌╣Ц┌╓Ц┌╨Ц│▄Х║▄Ц┌▐Ц┌▄Ц│╬Ц│≥Ц─┌
\n" +" " + +#: templates/mumble/mumble.html:103 +msgid "Server administration" +msgstr "Ц┌╣Ц┐╪Ц┐░Г╝║Г░├" + +#: templates/mumble/mumble.html:117 +msgid "Player" +msgstr "Ц┐≈Ц┐╛Ц┐╪Ц┐╓" + +#: templates/mumble/mumble.html:119 +msgid "Online since" +msgstr "Ф▌╔Г╤ И√▀Е╖▀Ф≥┌И√⌠" + +#: templates/mumble/mumble.html:120 templates/mumble/player.html:9 +msgid "Authenticated" +msgstr "Г≥╩И▄╡Ф╦┬Ц│©" + +#: templates/mumble/mumble.html:121 templates/mumble/mumble.html.py:136 +msgid "Admin" +msgstr "Г╝║Г░├Х─┘" + +#: templates/mumble/mumble.html:122 templates/mumble/player.html:12 +msgid "Muted" +msgstr "Г≥╨Х╗─Г╕│Ф╜╒" + +#: templates/mumble/mumble.html:123 templates/mumble/player.html:18 +msgid "Deafened" +msgstr "Х│╢Е▐√Г╕│Ф╜╒" + +#: templates/mumble/mumble.html:124 templates/mumble/player.html:21 +msgid "Muted by self" +msgstr "Х┤╙Е┬├Ц│╖Г≥╨Х╗─Г╕│Ф╜╒" + +#: templates/mumble/mumble.html:125 templates/mumble/player.html:24 +msgid "Deafened by self" +msgstr "Х┤╙Е┬├Ц│╖Х│╢Е▐√Г╕│Ф╜╒" + +#: templates/mumble/mumble.html:127 +msgid "IP Address" +msgstr "IPЦ┌╒Ц┐┴Ц┐╛Ц┌╧" + +#: templates/mumble/mumble.html:131 +msgid "User" +msgstr "Ц┐╕Ц┐╪Ц┌╤" + +#: templates/mumble/mumble.html:134 +msgid "Full Name" +msgstr "Е░█Е┴█" + +#: templates/mumble/mumble.html:137 +msgid "Sign-up date" +msgstr "Г≥╩И▄╡Ф≈╔" + +#: templates/mumble/mumble.html:142 +msgid "User Comment" +msgstr "Ц┐╕Ц┐╪Ц┌╤Ц┌╒Ц┌╚Ц┌╕Ц┐ЁЦ┐┬" + +#: templates/mumble/mumble.html:154 templates/mumble/mumble.html.py:168 +msgid "Kick user" +msgstr "Ц┐╕Ц┐╪Ц┌╤Ц┌▓Ц┌╜Ц┐┐Ц┌╞" + +#: templates/mumble/mumble.html:160 +msgid "Reason" +msgstr "Ф└▐Е▒Ё" + +#: templates/mumble/mumble.html:165 +msgid "Ban user" +msgstr "Ц┐╕Ц┐╪Ц┌╤Ц┌▓Х©╫Ф■╬" + +#: templates/mumble/mumble.html:175 +msgid "Channel" +msgstr "Ц┐│Ц┐ёЦ┐ЁЦ┐█Ц┐╚" + +#: templates/mumble/mumble.html:177 +msgid "Channel ID" +msgstr "Ц┐│Ц┐ёЦ┐ЁЦ┐█Ц┐╚ID" + +#: templates/mumble/mumble.html:179 +msgid "Connect" +msgstr "Ф▌╔Г╤ " + +#: templates/mumble/mumble.html:182 +msgid "Channel description" +msgstr "Ц┐│Ц┐ёЦ┐ЁЦ┐█Ц┐╚Х╙╛Ф≤▌" + +#: templates/mumble/mumble.html:229 +msgid "Delete" +msgstr "Е┴┼И≥╓" + +#: templates/mumble/mumble.html:265 +msgid "Server Info" +msgstr "Ц┌╣Ц┐╪Ц┐░Ф┐┘Е═╠" + +#: templates/mumble/mumble.html:266 +msgid "Registration" +msgstr "Г≥╩И▄╡" + +#: templates/mumble/mumble.html:275 +msgid "Administration" +msgstr "Г╝║Г░├" + +#: templates/mumble/mumble.html:282 +msgid "User List" +msgstr "Ц┐╕Ц┐╪Ц┌╤Д╦─Х╕╖" + +#: templates/mumble/mumble.html:286 +msgid "name" +msgstr "Е░█Е┴█" + +#: templates/mumble/mumble.html:306 +msgid "Change password" +msgstr "Ц┐▒Ц┌╧Ц┐╞Ц┐╪Ц┐┴" + +#: templates/mumble/mumble.html:319 +msgid "Add" +msgstr "Х©╫Е┼═" + +#: templates/mumble/mumble.html:331 +msgid "Save" +msgstr "Ц┌╩Ц┐╪Ц┐√" + +#: templates/mumble/mumble.html:357 +msgid "Resync with Murmur" +msgstr "MurmurЦ│╗Е├█Е░▄Ф°÷" + +#: templates/mumble/player.html:15 +msgid "Suppressed" +msgstr "И▌╝Ц┌│Ц┌┴Ц┌▄Ц│÷" + +#: templates/mumble/player.html:27 +msgid "has a User Comment set" +msgstr "Ц┌ЁЦ┐║Ц┐ЁЦ┐┬Ц┌▓Ц┌╩Ц┐┐Ц┐┬Ц│∙Ц┌▄Ц│╕Ц│└Ц│╬Ц│≥" + +#~ msgid "comment" +#~ msgstr "Ц┌ЁЦ┐║Ц┐ЁЦ┐┬" + +#~ msgid "hash" +#~ msgstr "Ц┐▐Ц┐┐Ц┌╥Ц┐╔" + +#~ msgid "Enter the ID of the default channel here. The Channel viewer displays the ID to server admins on the channel detail page." +#~ msgstr "Ф▌╔Г╤ И√▀Е╖▀Ф≥┌Ц│╚Е┬╘Г■╗Ц│∙Ц┌▄Ц┌▀Ц┐┤Ц┐∙Ц┌╘Ц┐╚Ц┐┬Ц┐│Ц┐ёЦ┐ЁЦ┐█Ц┐╚IDЦ┌▓Ф▄┤Е╝ Ц│≈Ц│╕Д╦▀Ц│∙Ц│└Ц─┌Ц┐│Ц┐ёЦ┐ЁЦ┐█Ц┐╚Х║╗Г╓╨Д╦─Х╕╖Ц│╚Ц│╕ Ц┐│Ц┐ёЦ┐ЁЦ┐█Ц┐╚Х╘ЁГ╢╟Ц┌▓Г╒╨Х╙█Ц│≥Ц┌▄Ц│╟Ц┐│Ц┐ёЦ┐ЁЦ┐█Ц┐╚IDЦ┌▓Х╙©Ц│╧Ц┌┴Ц┌▄Ц│╬Ц│≥Ц─┌." + +#~ msgid "The admin group was not found in the ACL's groups list!" +#~ msgstr "Г╝║Г░├Х─┘Ц┌╟Ц┐╚Ц┐╪Ц┐≈Ц│▄Ф╗╘И≥░Д╦─Х╕╖Ц│╚Х╕▀Е╫⌠Ц│÷Ц┌┼Ц│╬Ц│⌡Ц┌⌠." diff --git a/mumble/management/__init__.py b/mumble/management/__init__.py index b2c1b0f..49de3a9 100644 --- a/mumble/management/__init__.py +++ b/mumble/management/__init__.py @@ -14,9 +14,43 @@ * GNU General Public License for more details. """ -from server_detect import find_existing_instances -from django.db.models import signals -from mumble import models +from shutil import copy, move +from os.path import exists, join + +from django.conf import settings +from django.db import connection +from django.db.models import signals + +from mumble import models + +from update_schema import update_schema +from server_detect import find_existing_instances + + +if settings.DATABASE_ENGINE == "sqlite3": + # Move the DB to the db subdirectory if necessary. + oldpath = join( settings.MUMBLE_DJANGO_ROOT, "mumble-django.db3" ) + if not exists( settings.DATABASE_NAME ) and exists( oldpath ): + move( oldpath, settings.DATABASE_NAME ) + + +cursor = connection.cursor() + +tablename = models.Mumble._meta.db_table + +if tablename in connection.introspection.get_table_list(cursor): + fields = connection.introspection.get_table_description(cursor, tablename) + uptodate = "server_id" in [ entry[0] for entry in fields ] +else: + # Table doesn't yet exist, so syncdb will create it properly + uptodate = True + +if not uptodate: + if settings.DATABASE_ENGINE == "sqlite3": + # backup the db before the conversion. + copy( settings.DATABASE_NAME, settings.DATABASE_NAME+".bak" ) + signals.post_syncdb.connect( update_schema, sender=models ); +else: + signals.post_syncdb.connect( find_existing_instances, sender=models ); -signals.post_syncdb.connect( find_existing_instances, sender=models ); diff --git a/mumble/management/commands/checkenv.py b/mumble/management/commands/checkenv.py index a7867b2..c65baf4 100644 --- a/mumble/management/commands/checkenv.py +++ b/mumble/management/commands/checkenv.py @@ -14,7 +14,7 @@ * GNU General Public License for more details. """ -import os, Ice +import os from django.core.management.base import BaseCommand from django.contrib.auth.models import User @@ -28,8 +28,16 @@ class TestFailed( Exception ): pass; class Command( BaseCommand ): + help = "Run a few tests on Mumble-Django's setup." + def handle(self, **options): - self.check_slice(); + try: + import Ice + except ImportError: + pass + else: + self.check_slice(); + self.check_rootdir(); self.check_dbase(); self.check_sites(); @@ -166,10 +174,10 @@ class Command( BaseCommand ): else: for mumble in mm: try: - mumble.getCtl(); - except Ice.Exception, err: + mumble.ctl + except Exception, err: raise TestFailed( - "Connecting to Murmur `%s` (%s) failed: %s" % ( mumble.name, mumble.dbus, err ) + "Connecting to Murmur `%s` (%s) failed: %s" % ( mumble.name, mumble.server, err ) ); print "[ OK ]"; diff --git a/mumble/management/commands/getslice.py b/mumble/management/commands/getslice.py new file mode 100644 index 0000000..1de8b05 --- /dev/null +++ b/mumble/management/commands/getslice.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- + +""" + * Copyright б╘ 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +import Ice, IcePy, os, getpass +from sys import stderr + +from django.core.management.base import BaseCommand + +from mumble.models import MumbleServer + +class Command( BaseCommand ): + help = "Check if the known servers support getSlice." + + def handle(self, **options): + prop = Ice.createProperties([]) + prop.setProperty("Ice.ImplicitContext", "Shared") + + idd = Ice.InitializationData() + idd.properties = prop + + ice = Ice.initialize(idd) + + for serv in MumbleServer.objects.all(): + print "Probing server at '%s'..." % serv.dbus + + if serv.secret: + ice.getImplicitContext().put( "secret", serv.secret.encode("utf-8") ) + + prx = ice.stringToProxy( serv.dbus.encode("utf-8") ) + + # Try loading the Slice from Murmur directly via its getSlice method. + try: + slice = IcePy.Operation( 'getSlice', + Ice.OperationMode.Idempotent, Ice.OperationMode.Idempotent, + True, (), (), (), IcePy._t_string, () + ).invoke(prx, ((), None)) + except TypeError, err: + print " Received TypeError:", err + print " It seems your version of IcePy is incompatible." + except Ice.OperationNotExistException: + print " Your version of Murmur does not support getSlice." + else: + print " Successfully received the slice (length: %d bytes.)" % len(slice) diff --git a/mumble/management/server_detect.py b/mumble/management/server_detect.py index 5c033d5..769876e 100644 --- a/mumble/management/server_detect.py +++ b/mumble/management/server_detect.py @@ -14,11 +14,12 @@ * GNU General Public License for more details. """ -import os +import os, getpass +from django.db import DatabaseError from django.conf import settings -from mumble import models +from mumble.models import MumbleServer, Mumble from mumble.mctl import MumbleCtlBase @@ -61,7 +62,7 @@ def find_existing_instances( **kwargs ): print " 2) ICE -- Meta:tcp -h 127.0.0.1 -p 6502" print "Enter 1 or 2 for the defaults above, nothing to skip Server detection," print "and if the defaults do not fit your needs, enter the correct string." - print "Whether to use DBus or ICE will be detected automatically from the" + print "Whether to use DBus or Ice will be detected automatically from the" print "string's format." print @@ -78,8 +79,10 @@ def find_existing_instances( **kwargs ): elif dbusName == "2": dbusName = "Meta:tcp -h 127.0.0.1 -p 6502"; + icesecret = getpass.getpass("Please enter the Ice secret (if any): "); + try: - ctl = MumbleCtlBase.newInstance( dbusName, settings.SLICE ); + ctl = MumbleCtlBase.newInstance( dbusName, settings.SLICE, icesecret ); except Exception, instance: if v: print "Unable to connect using name %s. The error was:" % dbusName; @@ -92,40 +95,71 @@ def find_existing_instances( **kwargs ): servIDs = ctl.getAllServers(); + try: + meta = MumbleServer.objects.get( dbus=dbusName ); + except MumbleServer.DoesNotExist: + meta = MumbleServer( dbus=dbusName ); + finally: + meta.secret = icesecret; + meta.save(); + for id in servIDs: if v > 1: print "Checking Murmur instance with id %d." % id; # first check that the server has not yet been inserted into the DB try: - instance = models.Mumble.objects.get( dbus=dbusName, srvid=id ); - except models.Mumble.DoesNotExist: + instance = Mumble.objects.get( server=meta, srvid=id ); + except Mumble.DoesNotExist: values = { + "server": meta, "srvid": id, - "dbus": dbusName, } - if v > 1: - print "Found new Murmur instance %d on bus '%s'... " % ( id, dbusName ), + if v: + print "Found new Murmur instance %d on bus '%s'... " % ( id, dbusName ) # now create a model for the record set. - instance = models.Mumble( **values ); + instance = Mumble( **values ); else: - if v > 1: - print "Syncing Murmur instance... ", + if v: + print "Syncing Murmur instance %d: '%s'... " % ( instance.id, instance.name ) - instance.configureFromMurmur(); - - if v > 1: - print instance.name; - - instance.save( dontConfigureMurmur=True ); + try: + instance.configureFromMurmur(); + except DatabaseError, err: + try: + # Find instances with the same address/port + dup = Mumble.objects.get( addr=instance.addr, port=instance.port ) + except Mumble.DoesNotExist: + # None exist - this must've been something else. + print "Server ID / Name: %d / %s" % ( instance.srvid, instance.name ) + raise err + else: + print "ERROR: There is already another server instance registered" + print " on the same address and port." + print " -------------" + print " New Server ID:", instance.srvid, + print " New Server Name:", instance.name + print " Address:", instance.addr + print " Port:", instance.port + print " Connection string:", instance.server.dbus + print " -------------" + print " Duplicate Server ID:", dup.srvid, + print "Duplicate Server Name:", dup.name + print " Address:", dup.addr + print " Port:", dup.port + print " Connection string:", dup.server.dbus + return False + except Exception, err: + print "Server ID / Name: %d / %s" % ( instance.srvid, instance.name ) + raise err # Now search for players on this server that have not yet been registered if instance.booted: if v > 1: print "Looking for registered Players on Server id %d." % id; instance.readUsersFromMurmur( verbose=v ); - elif v > 1: + elif v: print "This server is not running, can't sync players."; if v > 1: diff --git a/mumble/management/update_schema.py b/mumble/management/update_schema.py new file mode 100644 index 0000000..edf62b2 --- /dev/null +++ b/mumble/management/update_schema.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- + +""" + * Copyright б╘ 2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +import os +from os.path import join + +from django.db import connection, transaction +from django.db.models import signals +from django.conf import settings + +from mumble import models +from mumble.management.server_detect import find_existing_instances + + +def update_schema( **kwargs ): + if "verbosity" in kwargs: + v = kwargs['verbosity']; + else: + v = 1; + + if v: + print "Migrating Database schema for Mumble-Django 2.0 now." + + scriptdir = join( settings.CONVERSIONSQL_ROOT, { + 'postgresql_psycopg2': 'pgsql', + 'postgresql': 'pgsql', + 'mysql': 'mysql', + 'sqlite3': 'sqlite', + }[settings.DATABASE_ENGINE] ) + + if v > 1: + print "Reading migration scripts for %s from '%s'" % ( settings.DATABASE_ENGINE, scriptdir ) + + scripts = [ filename for filename in os.listdir( scriptdir ) if filename.endswith( ".sql" ) ] + scripts.sort() + + for filename in scripts: + cursor = connection.cursor() + + scriptpath = os.path.join( scriptdir, filename ) + scriptfile = open( scriptpath, "r" ) + try: + if v > 1: + print "Running migration script '%s'..." % scriptpath + stmt = scriptfile.read() + cursor.execute( stmt ) + + except IOError, err: + print "Error reading file '%s':" % filename + print err + + except cursor.db.connection.Error, err: + print "Error executing file '%s':" % filename + print err + + finally: + scriptfile.close() + cursor.close() + + if v: + print "Database migration finished successfully." + + find_existing_instances( **kwargs ) diff --git a/mumble/mctl.py b/mumble/mctl.py index 502aff1..d7f030b 100644 --- a/mumble/mctl.py +++ b/mumble/mctl.py @@ -17,89 +17,22 @@ import re -class MumbleCtlBase (object): +class MumbleCtlBase(object): """ This class defines the base interface that the Mumble model expects. """ cache = {}; - def getAllConf(self, srvid): - raise NotImplementedError( "mctl::getAllConf" ); - - def getVersion( self ): - raise NotImplementedError( "mctl::getVersion" ); - - def getConf(self, srvid, key): - raise NotImplementedError( "mctl::getConf" ); - - def setConf(self, srvid, key, value): - raise NotImplementedError( "mctl::setConf" ); - - def getDefaultConf(self): - raise NotImplementedError( "mctl::getDefaultConf" ); - - def newServer(self): - raise NotImplementedError( "mctl::newServer" ); - - def setSuperUserPassword(self, srvid, value): - raise NotImplementedError( "mctl::setSuperUserPassword" ); - - def start(self, srvid): - raise NotImplementedError( "mctl::start" ); - - def stop(self, srvid): - raise NotImplementedError( "mctl::stop" ); - - def isBooted(self, srvid): - raise NotImplementedError( "mctl::isBooted" ); - - def deleteServer(self, srvid): - raise NotImplementedError( "mctl::deleteServer" ); - - def getPlayers(self, srvid): - raise NotImplementedError( "mctl::getPlayers" ); - - def getRegisteredPlayers(self, srvid, filter): - raise NotImplementedError( "mctl::getRegisteredPlayers" ); - - def getChannels(self, srvid): - raise NotImplementedError( "mctl::getChannels" ); - - def registerPlayer(self, srvid, name, email, password): - raise NotImplementedError( "mctl::registerPlayer" ); - - def getRegistration(self, srvid, mumbleid): - raise NotImplementedError( "mctl::getRegistration" ); - - def setRegistration(self, srvid, mumbleid, name, email, password): - raise NotImplementedError( "mctl::setRegistration" ); - - def unregisterPlayer(self, srvid, mumbleid): - raise NotImplementedError( "mctl::unregisterPlayer" ); - - def getBootedServers(self): - raise NotImplementedError( "mctl::getBootedServers" ); - - def getAllServers(self): - raise NotImplementedError( "mctl::getAllServers" ); - - def getACL(self, srvid, channelid): - raise NotImplementedError( "mctl::getACL" ); - - def setACL(self, srvid, channelid, acl, groups, inherit): - raise NotImplementedError( "mctl::setACL" ); - - def getTexture(self, srvid, mumbleid): - raise NotImplementedError( "mctl::getTexture" ); - - def setTexture(self, srvid, mumbleid, infile): - raise NotImplementedError( "mctl::setTexture" ); - - def verifyPassword( self, srvid, username, password ): - raise NotImplementedError( "mctl::verifyPassword" ); - @staticmethod - def newInstance( connstring, slicefile ): - """ Create a new CTL object for the given connstring. """ + def newInstance( connstring, slicefile=None, icesecret=None ): + """ Create a new CTL object for the given connstring. + + Optional parameters are the path to the slice file and the + Ice secret necessary to authenticate to Murmur. + + The path can be omitted only if using DBus or running Murmur + 1.2.3 or later, which exports a getSlice method to retrieve + the Slice from. + """ # check cache if connstring in MumbleCtlBase.cache: @@ -115,7 +48,7 @@ class MumbleCtlBase (object): ctl = MumbleCtlDbus( connstring ) else: from MumbleCtlIce import MumbleCtlIce - ctl = MumbleCtlIce( connstring, slicefile ) + ctl = MumbleCtlIce( connstring, slicefile, icesecret ) MumbleCtlBase.cache[connstring] = ctl; return ctl; @@ -123,6 +56,3 @@ class MumbleCtlBase (object): @staticmethod def clearCache(): MumbleCtlBase.cache = {}; - - - diff --git a/mumble/mmobjects.py b/mumble/mmobjects.py index 3c01e79..bde32cc 100644 --- a/mumble/mmobjects.py +++ b/mumble/mmobjects.py @@ -14,13 +14,23 @@ * GNU General Public License for more details. """ +import socket import datetime +import re from time import time -from os.path import join from django.utils.http import urlquote from django.conf import settings + +def cmp_channels( left, rite ): + """ Compare two channels, first by position, and if that equals, by name. """ + if hasattr( left, "position" ) and hasattr( rite, "position" ): + byorder = cmp( left.position, rite.position ); + if byorder != 0: + return byorder; + return cmp_names( left, rite ); + def cmp_names( left, rite ): """ Compare two objects by their name property. """ return cmp( left.name, rite.name ); @@ -65,7 +75,7 @@ class mmChannel( object ): return self._acl; - acl = property( getACL, doc=getACL.__doc__ ); + acl = property( getACL ); is_server = False; @@ -95,7 +105,7 @@ class mmChannel( object ): def sort( self ): """ Sort my subchannels and players, and then iterate over them and sort them recursively. """ - self.subchans.sort( cmp_names ); + self.subchans.sort( cmp_channels ); self.players.sort( cmp_names ); for subc in self.subchans: subc.sort(); @@ -113,26 +123,23 @@ class mmChannel( object ): """ Create an URL to connect to this channel. The URL is of the form mumble://username@host:port/parentchans/self.name """ - userstr = ""; + from urlparse import urlunsplit + versionstr = "version=%d.%d.%d" % tuple(self.server.version[:3]); + + if self.parent is not None: + chanlist = self.parent_channels() + [self.name]; + chanlist = [ urlquote( chan ) for chan in chanlist ]; + urlpath = "/".join( chanlist ); + else: + urlpath = ""; if for_user is not None: - userstr = "%s@" % for_user.name; - - versionstr = "version=%d.%d.%d" % tuple(self.server.version[0:3]); - - # create list of all my parents and myself - chanlist = self.parent_channels() + [self.name]; - # urlencode channel names - chanlist = [ urlquote( chan ) for chan in chanlist ]; - # create a path by joining the channel names - chanpath = join( *chanlist ); - - if self.server.port != settings.MUMBLE_DEFAULT_PORT: - return "mumble://%s%s:%d/%s?%s" % ( userstr, self.server.addr, self.server.port, chanpath, versionstr ); - - return "mumble://%s%s/%s?%s" % ( userstr, self.server.addr, chanpath, versionstr ); + netloc = "%s@%s" % ( for_user.name, self.server.netloc ); + return urlunsplit(( "mumble", netloc, urlpath, versionstr, "" )) + else: + return urlunsplit(( "mumble", self.server.netloc, urlpath, versionstr, "" )) - connecturl = property( getURL, doc="A convenience wrapper for getURL." ); + connecturl = property( getURL ); def setDefault( self ): """ Make this the server's default channel. """ @@ -149,7 +156,33 @@ class mmChannel( object ): chandata['players'] = [ pl.asDict() for pl in self.players ]; chandata['subchans'] = [ sc.asDict() for sc in self.subchans ]; return chandata; - + + def asMvXml( self, parentnode ): + """ Return an XML tree for this channel suitable for MumbleViewer-ng. """ + from xml.etree.cElementTree import SubElement + me = SubElement( parentnode, "item" , id=self.id, rel='channel' ) + content = SubElement( me, "content" ) + name = SubElement( content , "name" ) + name.text = self.name + + for sc in self.subchans: + sc.asMvXml(me) + for pl in self.players: + pl.asMvXml(me) + + def asMvJson( self ): + """ Return a Dict for this channel suitable for MumbleViewer-ng. """ + return { + "attributes": { + "href": self.connecturl, + "id": self.id, + "rel": "channel", + }, + "data": self.name, + "children": [ sc.asMvJson() for sc in self.subchans ] + \ + [ pl.asMvJson() for pl in self.players ], + "state": { False: "closed", True: "open" }[self.top_or_not_empty], + } @@ -201,6 +234,21 @@ class mmPlayer( object ): is_channel = False; is_player = True; + def getIpAsString( self ): + """ Get the client's IPv4 or IPv6 address, in a pretty format. """ + addr = self.player_obj.address; + if max( addr[:10] ) == 0 and addr[10:12] == (255, 255): + return "%d.%d.%d.%d" % tuple( addr[12:] ); + ip6addr = [(hi << 8 | lo) for (hi, lo) in zip(addr[0::2], addr[1::2])] + # colon-separated string: + ipstr = ':'.join([ ("%x" % part) for part in ip6addr ]); + # 0:0:0 -> :: + return re.sub( "((^|:)(0:){2,})", '::', ipstr, 1 ); + + ipaddress = property( getIpAsString ); + fqdn = property( lambda self: socket.getfqdn( self.ipaddress ), + doc="The fully qualified domain name of the user's host." ); + # kept for compatibility to mmChannel (useful for traversal funcs) playerCount = property( lambda self: -1, doc="Exists only for compatibility to mmChannel." ); @@ -221,6 +269,24 @@ class mmPlayer( object ): pldata['texture'] = self.mumbleuser.textureUrl; return pldata; + + def asMvXml( self, parentnode ): + """ Return an XML node for this player suitable for MumbleViewer-ng. """ + from xml.etree.cElementTree import SubElement + me = SubElement( parentnode, "item" , id=self.id, rel='user' ) + content = SubElement( me, "content" ) + name = SubElement( content , "name" ) + name.text = self.name + + def asMvJson( self ): + """ Return a Dict for this player suitable for MumbleViewer-ng. """ + return { + "attributes": { + "id": self.id, + "rel": "user", + }, + 'data': self.name, + } diff --git a/mumble/models.py b/mumble/models.py index 9804b13..86b7b96 100644 --- a/mumble/models.py +++ b/mumble/models.py @@ -14,9 +14,10 @@ * GNU General Public License for more details. """ -import socket +import socket, Ice, re +from sys import stderr -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext_noop, ugettext_lazy as _ from django.contrib.auth.models import User from django.db import models from django.db.models import signals @@ -26,20 +27,119 @@ from mumble.mmobjects import mmChannel, mmPlayer from mumble.mctl import MumbleCtlBase -def mk_config_property( field, doc="" ): +def mk_config_property( field, doc="", get_coerce=None, get_none=None, set_coerce=unicode, set_none='' ): """ Create a property for the given config field. """ def get_field( self ): if self.id is not None: - return self.getConf( field ) - else: - return None + val = self.getConf( field ); + if val is None or val == '': + return get_none + if callable(get_coerce): + return get_coerce( val ) + return val + return None def set_field( self, value ): - self.setConf( field, value ) + if value is None: + self.setConf( field, set_none ) + elif callable(set_coerce): + self.setConf( field, set_coerce(value) ) + else: + self.setConf( field, value ) return property( get_field, set_field, doc=doc ) +def mk_config_bool_property( field, doc="" ): + return mk_config_property( field, doc=doc, + get_coerce = lambda value: value == "true", + set_coerce = lambda value: str(value).lower() + ); + + +class MumbleServer( models.Model ): + """ Represents a Murmur server installation. """ + + dbus = models.CharField( _('DBus or ICE base'), max_length=200, unique=True, default=settings.DEFAULT_CONN, help_text=_( + "Examples: 'net.sourceforge.mumble.murmur' for DBus or 'Meta:tcp -h 127.0.0.1 -p 6502' for Ice.") ); + secret = models.CharField( _('Ice Secret'), max_length=200, blank=True ); + + class Meta: + verbose_name = _('Mumble Server'); + verbose_name_plural = _('Mumble Servers'); + + def __init__( self, *args, **kwargs ): + models.Model.__init__( self, *args, **kwargs ); + self._ctl = None; + self._conf = None; + + def __unicode__( self ): + return self.dbus; + + # Ctl instantiation + def getCtl( self ): + """ Instantiate and return a MumbleCtl object for this server. + + Only one instance will be created, and reused on subsequent calls. + """ + if not self._ctl: + self._ctl = MumbleCtlBase.newInstance( self.dbus, settings.SLICE, self.secret ); + return self._ctl; + + ctl = property( getCtl, doc="Get a Control object for this server. The ctl is cached for later reuse." ); + + def isMethodDbus(self): + """ Return true if this instance uses DBus. """ + rd = re.compile( r'^(\w+\.)*\w+$' ); + return bool(rd.match(self.dbus)) + + method_dbus = property( isMethodDbus ) + method_ice = property( lambda self: not self.isMethodDbus(), doc="Return true if this instance uses Ice." ) + + def getDefaultConf( self, field=None ): + """ Get a field from the default conf dictionary, or None if the field isn't set. """ + if self._conf is None: + self._conf = self.ctl.getDefaultConf() + if field is None: + return self._conf + if field in self._conf: + return self._conf[field] + return None + + def isOnline( self ): + """ Return true if this server process is running. """ + possibleexceptions = [] + try: + from Ice import ConnectionRefusedException + except ImportError, err: + if self.method_ice: + print >> stderr, err + return None + else: + possibleexceptions.append( ConnectionRefusedException ) + try: + from dbus import DBusException + except ImportError, err: + if self.method_dbus: + print >> stderr, err + return None + else: + possibleexceptions.append( DBusException ) + + try: + self.ctl + except tuple(possibleexceptions), err: + print >> stderr, err + return False + except (EnvironmentError, RuntimeError), err: + print >> stderr, err + return None + else: + return True + + online = property( isOnline ) + defaultconf = property( getDefaultConf, doc="The default config dictionary." ) + class Mumble( models.Model ): """ Represents a Murmur server instance. @@ -56,42 +156,49 @@ class Mumble( models.Model ): deleted as well. """ - name = models.CharField( _('Server Name'), max_length = 200 ); - dbus = models.CharField( _('DBus or ICE base'), max_length = 200, default = settings.DEFAULT_CONN, help_text=_( - "Examples: 'net.sourceforge.mumble.murmur' for DBus or 'Meta:tcp -h 127.0.0.1 -p 6502' for Ice.") ); - srvid = models.IntegerField( _('Server ID'), editable = False ); - addr = models.CharField( _('Server Address'), max_length = 200, help_text=_( - "Hostname or IP address to bind to. You should use a hostname here, because it will appear on the " - "global server list.") ); - port = models.IntegerField( _('Server Port'), default=settings.MUMBLE_DEFAULT_PORT, help_text=_( - "Port number to bind to. Use -1 to auto assign one.") ); - + server = models.ForeignKey( MumbleServer, verbose_name=_("Mumble Server") ); + name = models.CharField( _('Server Name'), max_length=200 ); + srvid = models.IntegerField( _('Server ID'), editable=False ); + addr = models.CharField( _('Server Address'), max_length=200, blank=True, help_text=_( + "Hostname or IP address to bind to. You should use a hostname here, because it will appear on the " + "global server list.") ); + port = models.IntegerField( _('Server Port'), blank=True, null=True, help_text=_( + "Port number to bind to. Leave empty to auto assign one.") ); + display = models.CharField( _('Server Display Address'), max_length=200, blank=True, help_text=_( + "This field is only relevant if you are located behind a NAT, and names the Hostname or IP address " + "to use in the Channel Viewer and for the global server list registration. If not given, the addr " + "and port fields are used. If display and bind ports are equal, you can omit it here.") ); supw = property( lambda self: '', - lambda self, value: self.ctl.setSuperUserPassword( self.srvid, value ), - doc='Superuser Password' + lambda self, value: ( value and self.ctl.setSuperUserPassword( self.srvid, value ) ) or None, + doc=_('Superuser Password') ) - url = mk_config_property( "registerurl", "Website URL" ) - motd = mk_config_property( "welcometext", "Welcome Message" ) - passwd = mk_config_property( "password", "Server Password" ) - users = mk_config_property( "users", "Max. Users" ) - bwidth = mk_config_property( "bandwidth", "Bandwidth [Bps]" ) - sslcrt = mk_config_property( "certificate", "SSL Certificate" ) - sslkey = mk_config_property( "key", "SSL Key" ) - player = mk_config_property( "username", "Player name regex" ) - channel = mk_config_property( "channelname", "Channel name regex" ) - defchan = mk_config_property( "defaultchannel", "Default channel" ) + url = mk_config_property( "registerurl", ugettext_noop("Website URL") ) + motd = mk_config_property( "welcometext", ugettext_noop("Welcome Message") ) + passwd = mk_config_property( "password", ugettext_noop("Server Password") ) + users = mk_config_property( "users", ugettext_noop("Max. Users"), get_coerce=int ) + bwidth = mk_config_property( "bandwidth", ugettext_noop("Bandwidth [Bps]"), get_coerce=int ) + sslcrt = mk_config_property( "certificate", ugettext_noop("SSL Certificate") ) + sslkey = mk_config_property( "key", ugettext_noop("SSL Key") ) + player = mk_config_property( "username", ugettext_noop("Player name regex") ) + channel = mk_config_property( "channelname", ugettext_noop("Channel name regex") ) + defchan = mk_config_property( "defaultchannel", ugettext_noop("Default channel"), get_coerce=int ) + timeout = mk_config_property( "timeout", ugettext_noop("Timeout"), get_coerce=int ) + + obfsc = mk_config_bool_property( "obfuscate", ugettext_noop("IP Obfuscation") ) + certreq = mk_config_bool_property( "certrequired", ugettext_noop("Require Certificate") ) + textlen = mk_config_bool_property( "textmessagelength", ugettext_noop("Maximum length of text messages") ) + html = mk_config_bool_property( "allowhtml", ugettext_noop("Allow HTML to be used in messages") ) + bonjour = mk_config_bool_property( "bonjour", ugettext_noop("Publish this server via Bonjour") ) + autoboot= mk_config_bool_property( "boot", ugettext_noop("Boot Server when Murmur starts") ) - obfsc = property( - lambda self: ( self.getConf( "obfuscate" ) == "true" ) if self.id is not None else None, - lambda self, value: self.setConf( "obfuscate", str(value).lower() ), - doc="IP Obfuscation" - ) - def getBooted( self ): if self.id is not None: - return self.ctl.isBooted( self.srvid ); + if self.server.online: + return self.ctl.isBooted( self.srvid ) + else: + return None else: return False @@ -102,10 +209,11 @@ class Mumble( models.Model ): else: self.ctl.stop( self.srvid ); - booted = property( getBooted, setBooted, doc="Boot Server" ) + booted = property( getBooted, setBooted, doc=ugettext_noop("Boot Server") ) + online = property( getBooted, setBooted, doc=ugettext_noop("Boot Server") ) class Meta: - unique_together = ( ( 'dbus', 'srvid' ), ( 'addr', 'port' ), ); + unique_together = ( ( 'server', 'srvid' ), ); verbose_name = _('Server instance'); verbose_name_plural = _('Server instances'); @@ -119,52 +227,33 @@ class Mumble( models.Model ): but to Murmur as well. """ if dontConfigureMurmur: - # skip murmur configuration, e.g. because we're inserting models for existing servers. return models.Model.save( self ); - # check if this server already exists, if not call newServer and set my srvid first - if self.id is None: self.srvid = self.ctl.newServer(); - if self.port == -1: - self.port = max( [ rec['port'] for rec in Mumble.objects.values('port') ] ) + 1; + self.ctl.setConf( self.srvid, 'registername', self.name ); - if self.port < 1 or self.port >= 2**16: - raise ValueError( _("Port number %(portno)d is not within the allowed range %(minrange)d - %(maxrange)d") % { - 'portno': self.port, - 'minrange': 1, - 'maxrange': 2**16, - }); - - self.ctl.setConf( self.srvid, 'host', socket.gethostbyname( self.addr ) ); - self.ctl.setConf( self.srvid, 'port', str(self.port) ); - self.ctl.setConf( self.srvid, 'registername', self.name ); - self.ctl.setConf( self.srvid, 'registerurl', self.url ); - - # registerHostname needs to take the port no into account - if self.port and self.port != settings.MUMBLE_DEFAULT_PORT: - self.ctl.setConf( self.srvid, 'registerhostname', "%s:%d" % ( self.addr, self.port ) ); + if self.addr and self.addr != '0.0.0.0': + self.ctl.setConf( self.srvid, 'host', socket.gethostbyname(self.addr) ); else: - self.ctl.setConf( self.srvid, 'registerhostname', self.addr ); + self.ctl.setConf( self.srvid, 'host', '' ); - if self.supw: - self.ctl.setSuperUserPassword( self.srvid, self.supw ); - self.supw = ''; + if self.port and self.port != settings.MUMBLE_DEFAULT_PORT + self.srvid - 1: + self.ctl.setConf( self.srvid, 'port', str(self.port) ); + else: + self.ctl.setConf( self.srvid, 'port', '' ); - if self.booted != self.ctl.isBooted( self.srvid ): - if self.booted: - self.ctl.start( self.srvid ); - else: - self.ctl.stop( self.srvid ); + if self.netloc: + self.ctl.setConf( self.srvid, 'registerhostname', self.netloc ); + else: + self.ctl.setConf( self.srvid, 'registerhostname', '' ); - # Now allow django to save the record set return models.Model.save( self ); def __init__( self, *args, **kwargs ): models.Model.__init__( self, *args, **kwargs ); - self._ctl = None; self._channels = None; self._rootchan = None; @@ -172,26 +261,14 @@ class Mumble( models.Model ): users_regged = property( lambda self: self.mumbleuser_set.count(), doc="Number of registered users." ); users_online = property( lambda self: len(self.ctl.getPlayers(self.srvid)), doc="Number of online users." ); channel_cnt = property( lambda self: len(self.ctl.getChannels(self.srvid)), doc="Number of channels." ); - is_public = property( lambda self: self.passwd == '', + is_public = property( lambda self: not self.passwd, doc="False if a password is needed to join this server." ); is_server = True; is_channel = False; is_player = False; - - # Ctl instantiation - def getCtl( self ): - """ Instantiate and return a MumbleCtl object for this server. - - Only one instance will be created, and reused on subsequent calls. - """ - if not self._ctl: - self._ctl = MumbleCtlBase.newInstance( self.dbus, settings.SLICE ); - return self._ctl; - - ctl = property( getCtl, doc="Get a Control object for this server. The ctl is cached for later reuse." ); - + ctl = property( lambda self: self.server.ctl ); def getConf( self, field ): return self.ctl.getConf( self.srvid, field ) @@ -200,38 +277,54 @@ class Mumble( models.Model ): return self.ctl.setConf( self.srvid, field, value ) def configureFromMurmur( self ): - default = self.ctl.getDefaultConf(); - conf = self.ctl.getAllConf( self.srvid ); + conf = self.ctl.getAllConf( self.srvid ); - def find_in_dicts( keys, valueIfNotFound=None ): - if not isinstance( keys, tuple ): - keys = ( keys, ); - - for keyword in keys: - if keyword in conf: - return conf[keyword]; - - for keyword in keys: - keyword = keyword.lower(); - if keyword in default: - return default[keyword]; - - return valueIfNotFound; + if "registername" not in conf or not conf["registername"]: + self.name = "noname"; + else: + self.name = conf["registername"]; - servername = find_in_dicts( "registername", "noname" ); - if not servername: - # RegistrationName was found in the dicts, but is an empty string - servername = "noname"; + if "registerhostname" in conf and conf["registerhostname"]: + if ':' in conf["registerhostname"]: + regname, regport = conf["registerhostname"].split(':') + regport = int(regport) + else: + regname = conf["registerhostname"] + regport = None + else: + regname = None + regport = None - addr = find_in_dicts( ( "registerhostname", "host" ), "0.0.0.0" ); - if addr.find( ':' ) != -1: - # The addr is a hostname which actually contains a port number, but we already got that from - # the port field, so we can simply drop it. - addr = addr.split(':')[0]; + if "host" in conf and conf["host"]: + addr = conf["host"] + else: + addr = None - self.name = servername; - self.addr = addr; - self.port = find_in_dicts( "port" ); + if "port" in conf and conf["port"]: + self.port = int(conf["port"]) + else: + self.port = None + + if regname and addr: + if regport == self.port: + if socket.gethostbyname(regname) == socket.gethostbyname(addr): + self.display = '' + self.addr = regname + else: + self.display = regname + self.addr = addr + else: + self.display = conf["registerhostname"] + self.addr = addr + elif regname and not addr: + self.display = regname + self.addr = '' + elif addr and not regname: + self.display = '' + self.addr = addr + else: + self.display = '' + self.addr = '' self.save( dontConfigureMurmur=True ); @@ -241,6 +334,9 @@ class Mumble( models.Model ): raise SystemError( "This murmur instance is not currently running, can't sync." ); players = self.ctl.getRegisteredPlayers(self.srvid); + known_ids = [rec["mumbleid"] + for rec in MumbleUser.objects.filter( server=self ).values( "mumbleid" ) + ] for idx in players: playerdata = players[idx]; @@ -248,12 +344,9 @@ class Mumble( models.Model ): if playerdata.userid == 0: # Skip SuperUsers continue; if verbose > 1: - print "Checking Player with id %d and name '%s'." % ( playerdata.userid, playerdata.name ); + print "Checking Player with id %d." % playerdata.userid; - try: - playerinstance = MumbleUser.objects.get( server=self, mumbleid=playerdata.userid ); - - except MumbleUser.DoesNotExist: + if playerdata.userid not in known_ids: if verbose: print 'Found new Player "%s".' % playerdata.name; @@ -267,8 +360,8 @@ class Mumble( models.Model ): else: if verbose > 1: - print "This player is already listed in the database."; - + print "Player '%s' is already known." % playerdata.name; + playerinstance = MumbleUser.objects.get( server=self, mumbleid=playerdata.userid ); playerinstance.name = playerdata.name; playerinstance.save( dontConfigureMurmur=True ); @@ -277,6 +370,8 @@ class Mumble( models.Model ): def isUserAdmin( self, user ): """ Determine if the given user is an admin on this server. """ if user.is_authenticated(): + if user.is_superuser: + return True; try: return self.mumbleuser_set.get( owner=user ).getAdmin(); except MumbleUser.DoesNotExist: @@ -355,31 +450,87 @@ class Mumble( models.Model ): channels = property( getChannels, doc="A convenience wrapper for getChannels()." ); rootchan = property( lambda self: self.channels[0], doc="A convenience wrapper for getChannels()[0]." ); + def getNetloc( self ): + """ Return the address from the Display field (if any), or the server address. + Users from outside a NAT will need to use the Display address to connect + to this server instance. + """ + if self.display: + if ":" in self.display: + return self.display; + else: + daddr = self.display; + else: + daddr = self.addr; + + if self.port and self.port != settings.MUMBLE_DEFAULT_PORT: + return "%s:%d" % (daddr, self.port); + else: + return daddr; + + netloc = property( getNetloc ); + def getURL( self, forUser = None ): """ Create an URL of the form mumble://username@host:port/ for this server. """ - userstr = ""; + if not self.netloc: + return None + from urlparse import urlunsplit + versionstr = "version=%d.%d.%d" % tuple(self.version[:3]); if forUser is not None: - userstr = "%s@" % forUser.name; - - versionstr = "version=%d.%d.%d" % tuple(self.version[0:3]); - - if self.port != settings.MUMBLE_DEFAULT_PORT: - return "mumble://%s%s:%d/?%s" % ( userstr, self.addr, self.port, versionstr ); - - return "mumble://%s%s/?%s" % ( userstr, self.addr, versionstr ); + netloc = "%s@%s" % ( forUser.name, self.netloc ); + return urlunsplit(( "mumble", netloc, "", versionstr, "" )) + else: + return urlunsplit(( "mumble", self.netloc, "", versionstr, "" )) - connecturl = property( getURL, doc="A convenience wrapper for getURL()." ); + connecturl = property( getURL ); - version = property( lambda self: self.ctl.getVersion(), doc="The version of Murmur." ); + version = property( lambda self: self.ctl.getVersion(), doc="The version of Murmur." ); def asDict( self ): return { 'name': self.name, 'id': self.id, 'root': self.rootchan.asDict() }; + + def asMvXml( self ): + """ Return an XML tree for this server suitable for MumbleViewer-ng. """ + from xml.etree.cElementTree import Element + root = Element("root") + self.rootchan.asMvXml(root) + return root + + def asMvJson( self ): + """ Return a Dict for this server suitable for MumbleViewer-ng. """ + return self.rootchan.asMvJson() + + # "server" field protection + def __setattr__( self, name, value ): + if name == 'server': + if self.id is not None and self.server != value: + raise AttributeError( _( "This field must not be updated once the record has been saved." ) ); + + models.Model.__setattr__( self, name, value ); + + def kickUser( self, sessionid, reason="" ): + return self.ctl.kickUser( self.srvid, sessionid, reason ); + + def banUser( self, sessionid, reason="" ): + return self.ctl.addBanForSession( self.srvid, sessionid, reason=reason ); +def mk_registration_property( field, doc="" ): + """ Create a property for the given registration field. """ + + def get_field( self ): + if "comment" in self.registration: + return self.registration["comment"]; + else: + return None; + + return property( get_field, doc=doc ) + + class MumbleUser( models.Model ): """ Represents a User account in Murmur. @@ -399,6 +550,9 @@ class MumbleUser( models.Model ): server = models.ForeignKey( Mumble, verbose_name=_('Server instance'), related_name="mumbleuser_set" ); owner = models.ForeignKey( User, verbose_name=_('Account owner'), related_name="mumbleuser_set", null=True, blank=True ); + comment = mk_registration_property( "comment", doc=ugettext_noop("The user's comment.") ); + hash = mk_registration_property( "hash", doc=ugettext_noop("The user's hash.") ); + class Meta: unique_together = ( ( 'server', 'owner' ), ( 'server', 'mumbleid' ) ); verbose_name = _( 'User account' ); @@ -418,10 +572,8 @@ class MumbleUser( models.Model ): def save( self, dontConfigureMurmur=False ): """ Save the settings in this model to Murmur. """ if dontConfigureMurmur: - # skip murmur configuration, e.g. because we're inserting models for existing players. return models.Model.save( self ); - # Before the record set is saved, update Murmur via controller. ctl = self.server.ctl; if self.owner: @@ -451,7 +603,6 @@ class MumbleUser( models.Model ): # Don't save the users' passwords, we don't need them anyway self.password = ''; - # Now allow django to save the record set return models.Model.save( self ); def __init__( self, *args, **kwargs ): @@ -461,10 +612,15 @@ class MumbleUser( models.Model ): # Admin handlers def getAdmin( self ): """ Get ACL of root Channel, get the admin group and see if this user is in it. """ - return self.server.rootchan.acl.group_has_member( "admin", self.mumbleid ); + if self.mumbleid == -1: + return False; + else: + return self.server.rootchan.acl.group_has_member( "admin", self.mumbleid ); def setAdmin( self, value ): """ Set or revoke this user's membership in the admin group on the root channel. """ + if self.mumbleid == -1: + return False; if value: self.server.rootchan.acl.group_add_member( "admin", self.mumbleid ); else: @@ -472,7 +628,8 @@ class MumbleUser( models.Model ): self.server.rootchan.acl.save(); return value; - aclAdmin = property( getAdmin, setAdmin, doc="Wrapper around getAdmin/setAdmin (not a database field like isAdmin)" ); + aclAdmin = property( getAdmin, setAdmin, doc=ugettext_noop('Admin on root channel') ); + # Registration fetching def getRegistration( self ): @@ -481,38 +638,19 @@ class MumbleUser( models.Model ): self._registration = self.server.ctl.getRegistration( self.server.srvid, self.mumbleid ); return self._registration; - registration = property( getRegistration, doc=getRegistration.__doc__ ); - - def getComment( self ): - """ Retrieve a user's comment, if any. """ - if "comment" in self.registration: - return self.registration["comment"]; - else: - return None; - - comment = property( getComment, doc=getComment.__doc__ ); - - def getHash( self ): - """ Retrieve a user's hash, if any. """ - if "hash" in self.registration: - return self.registration["hash"]; - else: - return None; - - hash = property( getHash, doc=getHash.__doc__ ); + registration = property( getRegistration ); # Texture handlers - def getTexture( self ): """ Get the user texture as a PIL Image. """ return self.server.ctl.getTexture(self.server.srvid, self.mumbleid); - def setTexture( self, infile ): - """ Read an image from the infile and install it as the user's texture. """ - self.server.ctl.setTexture(self.server.srvid, self.mumbleid, infile) + def setTexture( self, image ): + """ Install the given image as the user's texture. """ + self.server.ctl.setTexture(self.server.srvid, self.mumbleid, image) texture = property( getTexture, setTexture, - doc="Get the texture as a PIL Image or read from a file (pass the path)." + doc="Get the texture as a PIL Image or set the Image as the texture." ); def hasTexture( self ): @@ -530,10 +668,10 @@ class MumbleUser( models.Model ): from django.core.urlresolvers import reverse return reverse( showTexture, kwargs={ 'server': self.server.id, 'userid': self.id } ); - textureUrl = property( getTextureUrl, doc=getTextureUrl.__doc__ ); + textureUrl = property( getTextureUrl ); + # Deletion handler - @staticmethod def pre_delete_listener( **kwargs ): kwargs['instance'].unregister(); @@ -546,7 +684,6 @@ class MumbleUser( models.Model ): # "server" field protection - def __setattr__( self, name, value ): if name == 'server': if self.id is not None and self.server != value: diff --git a/mumble/templates/mumble/channel.html b/mumble/templates/mumble/channel.html new file mode 100644 index 0000000..4ec00c2 --- /dev/null +++ b/mumble/templates/mumble/channel.html @@ -0,0 +1,28 @@ +{% comment %} + +{% endcomment %} +{% load mumble_extras %} +
+ + {% if Channel.linked %} + linked channel + {% else %} + channel + {% endif %} + {% if Channel.server.netloc %} + + {{ Channel.name|trunc:30 }} + + {% else %} + + {{ Channel.name|trunc:30 }} + + {% endif %} + {% for sub in Channel.subchans %} + {% if sub.show %} + {{ sub|chanview:MumbleAccount }} + {% endif %} + {% endfor %} + {% for player in Channel.players %}{{ player|chanview }}{% endfor %} +
+ diff --git a/mumble/templates/mumble/list.html b/mumble/templates/mumble/list.html new file mode 100644 index 0000000..b82bd74 --- /dev/null +++ b/mumble/templates/mumble/list.html @@ -0,0 +1,24 @@ +{% extends "index.html" %} +{% comment %} + +{% endcomment %} +{% load i18n %} +{% load mumble_extras %} +{% block Headline %} +Configured Mumble Servers +{% endblock %} +{% block Content %} +
+
    + {% for mumble in MumbleObjects %} + {% if mumble.booted %} +
  • {{ mumble.name }}
  • + {% else %} +
  • {{ mumble.name }} (offline)
  • + {% endif %} + {% empty %} + {% trans "No server instances have been configured yet." %} + {% endfor %} +
+
+{% endblock %} diff --git a/mumble/templates/mumble/mobile_list.html b/mumble/templates/mumble/mobile_list.html new file mode 100644 index 0000000..06738f0 --- /dev/null +++ b/mumble/templates/mumble/mobile_list.html @@ -0,0 +1,21 @@ +{% extends "mobile_index.html" %} +{% comment %} + +{% endcomment %} +{% load mumble_extras %} +{% block Headline %} +Configured Mumble Servers +{% endblock %} +{% block LeftColumn %} +
+
    + {% for mumble in MumbleObjects %} + {% if mumble.booted %} +
  • {{ mumble.name }}
  • + {% else %} +
  • {{ mumble.name }} (offline)
  • + {% endif %} + {% endfor %} +
+
+{% endblock %} diff --git a/mumble/templates/mumble/mobile_mumble.html b/mumble/templates/mumble/mobile_mumble.html new file mode 100644 index 0000000..a0bc18f --- /dev/null +++ b/mumble/templates/mumble/mobile_mumble.html @@ -0,0 +1,12 @@ +{% extends "mobile_index.html" %} +{% comment %} + +{% endcomment %} +{% load mumble_extras %} +{% load i18n %} +{% block Headline %} + {{ DBaseObject.name }} +{% endblock %} +{% block LeftColumn %} + {{ DBaseObject|chanview:MumbleAccount }} +{% endblock %} diff --git a/mumble/templates/mumble/mumble.html b/mumble/templates/mumble/mumble.html new file mode 100644 index 0000000..433d8ac --- /dev/null +++ b/mumble/templates/mumble/mumble.html @@ -0,0 +1,388 @@ +{% extends "index.html" %} +{% comment %} + +{% endcomment %} +{% load mumble_extras %} +{% load i18n %} +{% block Headline %} + {{ DBaseObject.name }} +{% endblock %} +{% block LeftColumn %} + {{ DBaseObject|chanview:MumbleAccount }} +{% endblock %} +{% block Content %} + +
+
+
    + {% if DBaseObject.connecturl %} +
  • {% trans "Server Address" %}: {{ DBaseObject.connecturl }}
  • + {% endif %} + {% if DBaseObject.url %} +
  • {% trans "Website" %}: {{ DBaseObject.url|urlize }}
  • + {% endif %} +
  • {% trans "Server version" %}: {{ DBaseObject.version.0 }}.{{ DBaseObject.version.1 }}.{{ DBaseObject.version.2 }}
  • +
  • {% trans "Minimal view" %}
  • +
+
+ {% trans "Welcome message" %} + {{ DBaseObject.motd|removetags:"script link meta html head body style"|safe }} +
+
+
+ {% if user.is_authenticated %} +

{% trans "Server registration" %}

+
+ {% if Registered %} + {% trans "You are registered on this server" %}.
+ {% else %} + {% trans "You do not have an account on this server" %}.
+ {% endif %} + + {{ RegForm }} +
+ + +
+ {% else %} + {% blocktrans %} +

You need to be logged in to be able to register an account on this Mumble server.

+ {% endblocktrans %} + {% endif %} +
+ + {% if Registered %} +
+

{% trans "User Texture" %}

+ {% if DBaseObject|mmversion_eq:"1.2.2" %} + {% blocktrans %} + Sorry, due to a bug in Murmur 1.2.2, displaying and setting the Texture is disabled. + {% endblocktrans %} + {% else %} +

+ {% blocktrans with DBaseObject.id as serverid %} + You can upload an image that you would like to use as your user texture here. + {% endblocktrans %}
+
+ {% if MumbleAccount.hasTexture %} + {% trans "Your current texture is" %}:
+ user texture
+ {% else %} + {% trans "You don't currently have a texture set" %}.
+ {% endif %} +
+ {% if DBaseObject|mmversion_lt:"1.2.3" %} + {% blocktrans with DBaseObject.id as serverid %} + Hint: The texture image needs to be 600x60 in size. If you upload an image with + a different size, it will be resized accordingly.
+ {% endblocktrans %} + {% endif %} +

+
+ + {{ TextureForm }} +
+ + +
+ {% endif %} +
+ {% endif %} + + {% if CurrentUserIsAdmin %} +
+

{% trans "Server administration" %}

+
+ + {{ AdminForm }} +
+ + +
+
+ {% endif %} + + {% for item in ChannelTable %} + {% if item.is_player %} +
+

{% trans "Player" %} {{ item.name }}

+
    +
  • {% trans "Online since" %}: {{ item.onlinesince|time }}
  • +
  • {% trans "Authenticated" %}: {{ item.isAuthed|yesno }}
  • +
  • {% trans "Admin" %}: {{ item.isAdmin|yesno }}
  • +
  • {% trans "Muted" %}: {{ item.mute|yesno }}
  • +
  • {% trans "Deafened" %}: {{ item.deaf|yesno }}
  • +
  • {% trans "Muted by self" %}: {{ item.selfMute|yesno }}
  • +
  • {% trans "Deafened by self" %}: {{ item.selfDeaf|yesno }}
  • + {% if CurrentUserIsAdmin or user.is_staff %} +
  • {% trans "IP Address" %}: {{ item.fqdn }}
  • + {% endif %} +
+ {% if item.mumbleuser and item.mumbleuser.owner %} +

{% trans "User" %} {{ item.mumbleuser.owner.username|capfirst }}

+
    + {% if item.mumbleuser.owner.first_name and item.mumbleuser.owner.last_name %} +
  • {% trans "Full Name" %}: {{ item.mumbleuser.owner.first_name }} {{ item.mumbleuser.owner.last_name }}
  • + {% endif %} +
  • {% trans "Admin" %}: {{ item.mumbleuser.owner.is_staff|yesno }}
  • +
  • {% trans "Sign-up date" %}: {{ item.mumbleuser.owner.date_joined|date }}
  • +
+ {% endif %} + {% if item.comment %} +
+ {% trans "User Comment" %} + {{ item.comment|removetags:"script link meta html head body style"|safe }} +
+ {% endif %} + {% if item.mumbleuser and item.mumbleuser.hasTexture %} +
+ {% trans "User Texture" %} + user texture +
+ {% endif %} + {% if CurrentUserIsAdmin or user.is_staff %} +
+ {% trans "Kick user" %} +
+ + +
    +
  • + + +
  • +
  • + + +
  • +
+ +
+
+ {% endif %} +
+ {% else %} +
+

{% trans "Channel" %} {{ item.name }}

+ {% if CurrentUserIsAdmin or user.is_staff %} + {% trans "Channel ID" %}: {{ item.chanid }}
+ {% endif %} + {% trans "Connect" %} + {% if item.description %} +
+ {% trans "Channel description" %} + {{ item.description|removetags:"script link meta html head body style"|safe }} +
+ {% endif %} +
+ {% endif %} + {% endfor %} +{% endblock %} + +{% block HeadTag %} + + +{% endblock %} diff --git a/mumble/templates/mumble/offline.html b/mumble/templates/mumble/offline.html new file mode 100644 index 0000000..43b1c5d --- /dev/null +++ b/mumble/templates/mumble/offline.html @@ -0,0 +1,14 @@ +{% extends "index.html" %} +{% comment %} + +{% endcomment %} +{% load i18n %} +{% load mumble_extras %} +{% block Headline %} + {{ DBaseObject.name }} +{% endblock %} +{% block Content %} +
+ {% trans "This server is currently offline." %} +
+{% endblock %} diff --git a/mumble/templates/mumble/player.html b/mumble/templates/mumble/player.html new file mode 100644 index 0000000..d494625 --- /dev/null +++ b/mumble/templates/mumble/player.html @@ -0,0 +1,35 @@ +{% comment %} + +{% endcomment %} +{% load mumble_extras %} +{% load i18n %} +
+ + {% if Player.isAuthed %} + authed + {% endif %} + {% if Player.mute %} + muted + {% endif %} + {% if Player.suppress %} + muted + {% endif %} + {% if Player.deaf %} + deafened + {% endif %} + {% if Player.selfMute %} + self-muted + {% endif %} + {% if Player.selfDeaf %} + self-deafened + {% endif %} + {% if Player.hasComment %} + has comment + {% endif %} + + + + Player + {{ Player.name|trunc:30 }} + +
diff --git a/mumble/templates/mumble/server.html b/mumble/templates/mumble/server.html new file mode 100644 index 0000000..9c0ef67 --- /dev/null +++ b/mumble/templates/mumble/server.html @@ -0,0 +1,14 @@ +{% comment %} + +{% endcomment %} +{% load mumble_extras %} + +{% for sub in Server.rootchan.subchans %} + {% if sub.show %} + {{ sub|chanview:MumbleAccount }} + {% endif %} +{% endfor %} +{% for player in Server.rootchan.players %}{{ player|chanview }}{% endfor %} diff --git a/mumble/templatetags/__init__.py b/mumble/templatetags/__init__.py new file mode 100644 index 0000000..cb703eb --- /dev/null +++ b/mumble/templatetags/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +""" + * Copyright б╘ 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" diff --git a/mumble/templatetags/mumble_extras.py b/mumble/templatetags/mumble_extras.py new file mode 100644 index 0000000..2398aba --- /dev/null +++ b/mumble/templatetags/mumble_extras.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +""" + * Copyright б╘ 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +from django import template +from django.template.loader import render_to_string + +from django.conf import settings + +register = template.Library(); + + +@register.filter +def trunc( string, maxlen = 50 ): + """ converts "a very very extaordinary long text" to "a very very extra... """ + if len(string) < maxlen: + return string; + return string[:(maxlen - 3)] + "Б─╕"; + + +@register.filter +def chanview( obj, user = None ): + """ renders an mmChannel / mmPlayer object with the correct template """ + if obj.is_server: + return render_to_string( 'mumble/server.html', { 'Server': obj, 'MumbleAccount': user, 'MEDIA_URL': settings.MEDIA_URL } ); + elif obj.is_channel: + return render_to_string( 'mumble/channel.html', { 'Channel': obj, 'MumbleAccount': user, 'MEDIA_URL': settings.MEDIA_URL } ); + elif obj.is_player: + return render_to_string( 'mumble/player.html', { 'Player': obj, 'MumbleAccount': user, 'MEDIA_URL': settings.MEDIA_URL } ); + + +@register.filter +def chanurl( obj, user ): + """ create a connection URL and takes the user's login into account """ + return obj.getURL( user ); + +@register.filter +def mmversion_lt( obj, version ): + """ return True if the given Server's version is less than the given version. """ + return tuple(obj.version[:3]) < tuple([int(v) for v in version.split('.')]) + +@register.filter +def mmversion_eq( obj, version ): + """ return True if the given Server's version equals the given version. """ + return tuple(obj.version[:3]) == tuple([int(v) for v in version.split('.')]) + +@register.filter +def mmversion_gt( obj, version ): + """ return True if the given Server's version is greater than the given version. """ + return tuple(obj.version[:3]) > tuple([int(v) for v in version.split('.')]) diff --git a/mumble/testrunner.py b/mumble/testrunner.py new file mode 100644 index 0000000..e1e30c1 --- /dev/null +++ b/mumble/testrunner.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- + +""" + * Copyright б╘ 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +import os + +from django.test.simple import run_tests as django_run_tests +from django.conf import settings + +from murmurenvutils import get_available_versions, run_callback, wait_for_user + + +def run_tests( test_labels, verbosity=1, interactive=True, extra_tests=[] ): + """ Run the Django built in testing framework, but before testing the mumble + app, allow Murmur to be set up correctly. + """ + + if not test_labels: + test_labels = [ appname.split('.')[-1] for appname in settings.INSTALLED_APPS ]; + + # No need to sync any murmur servers for the other apps + os.environ['MURMUR_CONNSTR'] = ''; + + # The easy way: mumble is not being tested. + if "mumble" not in test_labels: + return django_run_tests( test_labels, verbosity, interactive, extra_tests ); + + # First run everything apart from mumble. mumble will be tested separately, so Murmur + # can be set up properly first. + failed_tests = 0; + + if len(test_labels) > 1: + # only run others if mumble is not the only app to be tested + test_labels = list(test_labels); + test_labels.remove( "mumble" ); + failed_tests += django_run_tests( test_labels, verbosity, interactive, extra_tests ); + + failed_tests += run_mumble_tests( verbosity, interactive ); + + return failed_tests; + + +def run_mumble_tests( verbosity=1, interactive=True ): + + connstrings = { + 'DBus': 'net.sourceforge.mumble.murmur', + 'Ice': 'Meta:tcp -h 127.0.0.1 -p 6502', + }; + + def django_run_tests_wrapper( process, version ): + wr_failed_tests = 0; + + for method in connstrings: + # Check if this server is ready to be used with the current method + if getattr( process.capabilities, ("has_%s" % method.lower()), False ): + print "Testing mumble %s via %s" % ( version, method ); + + os.environ['MURMUR_CONNSTR'] = connstrings[method]; + settings.DEFAULT_CONN = connstrings[method]; + settings.SLICE_VERSION = [ int(dgt) for dgt in version.split('.') ]; + + print "MURMUR_CONNSTR:", os.environ['MURMUR_CONNSTR']; + print "DEFAULT_CONN: ", settings.DEFAULT_CONN; + print "SLICE_VERSION: ", settings.SLICE_VERSION; + + if not process.capabilities.has_users: + print "Waiting for user to connect (60 seconds)." + wait_for_user( process, timeout=60 ); + + wr_failed_tests += django_run_tests( ('mumble',), verbosity, interactive, [] ); + else: + print "Mumble %s does not support Method %s" % ( version, method ); + + return wr_failed_tests; + + failed_tests = 0; + + from mctl import MumbleCtlBase + + for version in get_available_versions(): + MumbleCtlBase.clearCache(); + + run = raw_input( "Run tests for %s? [Y/n] " % version ); + if run in ('Y', 'y', ''): + failed_tests += run_callback( version, django_run_tests_wrapper, version ); + + return failed_tests; diff --git a/mumble/tests.py b/mumble/tests.py new file mode 100644 index 0000000..eaedc84 --- /dev/null +++ b/mumble/tests.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- + +""" + * Copyright б╘ 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +from django.conf import settings +from django.test import TestCase + +from models import Mumble +from utils import ObjectInfo + + +class InstancesHandling( TestCase ): + """ Tests creation, editing and removing of vserver instances. """ + + def setUp( self ): + # Make sure we always start with a FRESH murmur instance, checking for left-over instances + # and deleting them before creating ours. + try: + self.murmur = Mumble.objects.get( addr="0.0.0.0", port=31337 ); + except Mumble.DoesNotExist: + pass + else: + self.murmur.delete(); + finally: + self.murmur = Mumble( name="#unit testing instance#", addr="0.0.0.0", port=31337 ); + self.murmur.save(); + + def testDefaultConf( self ): + conf = self.murmur.ctl.getAllConf( self.murmur.srvid ); + + self.assert_( type(conf) == dict ); + self.assert_( "host" in conf ); + self.assert_( "port" in conf ); + self.assert_( "certificate" in conf ); + self.assert_( "key" in conf ); + self.assert_( "registerhostname" in conf ); + self.assert_( "registername" in conf ); + self.assert_( "channelname" in conf ); + self.assert_( "username" in conf ); + self.assert_( "obfuscate" in conf ); + self.assert_( "defaultchannel" in conf ); + + def testAddrPortUnique( self ): + try: + duplicate = Mumble( + name="#another unit testing instance#", + addr=self.murmur.addr, port=self.murmur.port, + dbus=settings.DEFAULT_CONN + ); + if duplicate.ctl.method == "ICE": + import Murmur + self.assertRaises( Murmur.ServerFailureException, duplicate.save ); + elif self.murmur.version[:2] == [ 1, 2 ]: + from dbus import DBusException + self.assertRaises( DBusException, duplicate.save ); + else: + from sqlite3 import IntegrityError + self.assertRaises( IntegrityError, duplicate.save ); + finally: + # make sure the duplicate is removed + duplicate.ctl.deleteServer( duplicate.srvid ); + + def tearDown( self ): + self.murmur.delete(); + + +class DataReading( TestCase ): + """ Tests reading data from murmur using the low-level CTL methods. """ + + def setUp( self ): + # BIG FAT WARNING: This sucks ass, because it assumes the tester has a + # Murmur database like the one I have. + # I definitely need to prepare Murmur somehow before running these tests. + # Just don't yet know how. + self.murmur = Mumble.objects.get(id=1); + + + def testCtlGetChannels( self ): + """ Test getChannels() """ + + channels = self.murmur.ctl.getChannels( self.murmur.srvid ); + + if self.murmur.ctl.method == "ICE": + import Murmur + self.assertEquals( type( channels[0] ), Murmur.Channel ); + else: + self.assertEquals( type( channels[0] ), ObjectInfo ); + + self.assert_( hasattr( channels[0], "id" ) ); + self.assert_( hasattr( channels[0], "name" ) ); + self.assert_( hasattr( channels[0], "parent" ) ); + self.assert_( hasattr( channels[0], "links" ) ); + + + def testCtlGetPlayers( self ): + """ Test getPlayers() """ + + players = self.murmur.ctl.getPlayers( self.murmur.srvid ); + + self.assert_( len(players) > 0 ); + + self.assertEquals( type(players), dict ); + + for plidx in players: + player = players[plidx]; + + if self.murmur.ctl.method == "ICE" and self.murmur.version[:2] == ( 1, 2 ): + import Murmur + self.assertEquals( type( player ), Murmur.User ); + else: + self.assertEquals( type( player ), ObjectInfo ); + + self.assert_( hasattr( player, "session" ) ); + self.assert_( hasattr( player, "mute" ) ); + self.assert_( hasattr( player, "deaf" ) ); + self.assert_( hasattr( player, "selfMute" ) ); + self.assert_( hasattr( player, "selfDeaf" ) ); + self.assert_( hasattr( player, "channel" ) ); + self.assert_( hasattr( player, "userid" ) ); + self.assert_( hasattr( player, "name" ) ); + self.assert_( hasattr( player, "onlinesecs" ) ); + self.assert_( hasattr( player, "bytespersec" ) ); + + + def testCtlGetRegisteredPlayers( self ): + """ Test getRegistredPlayers() and getRegistration() """ + + players = self.murmur.ctl.getRegisteredPlayers( self.murmur.srvid ); + + self.assert_( len(players) > 0 ); + + self.assertEquals( type(players), dict ); + + for plidx in players: + player = players[plidx]; + + self.assertEquals( type( player ), ObjectInfo ); + + self.assert_( hasattr( player, "userid" ) ); + self.assert_( hasattr( player, "name" ) ); + self.assert_( hasattr( player, "email" ) ); + self.assert_( hasattr( player, "pw" ) ); + + # compare with getRegistration result + reg = self.murmur.ctl.getRegistration( self.murmur.srvid, player.userid ); + + self.assertEquals( type( reg ), ObjectInfo ); + + self.assert_( hasattr( reg, "userid" ) ); + self.assert_( hasattr( reg, "name" ) ); + self.assert_( hasattr( reg, "email" ) ); + self.assert_( hasattr( reg, "pw" ) ); + + self.assertEquals( player.userid, reg.userid ); + self.assertEquals( player.name, reg.name ); + self.assertEquals( player.email, reg.email ); + self.assertEquals( player.pw, reg.pw ); + + + def testCtlGetAcl( self ): + """ Test getACL() for the root channel """ + + acls, groups, inherit = self.murmur.ctl.getACL( self.murmur.srvid, 0 ); + + for rule in acls: + if self.murmur.ctl.method == "ICE" and self.murmur.version[:2] == ( 1, 2 ): + import Murmur + self.assertEquals( type( rule ), Murmur.ACL ); + else: + self.assertEquals( type( rule ), ObjectInfo ); + + self.assert_( hasattr( rule, "applyHere" ) ); + self.assert_( hasattr( rule, "applySubs" ) ); + self.assert_( hasattr( rule, "inherited" ) ); + self.assert_( hasattr( rule, "userid" ) ); + self.assert_( hasattr( rule, "group" ) ); + self.assert_( hasattr( rule, "allow" ) ); + self.assert_( hasattr( rule, "deny" ) ); + + for grp in groups: + if self.murmur.ctl.method == "ICE": + import Murmur + self.assertEquals( type( grp ), Murmur.Group ); + else: + self.assertEquals( type( grp ), ObjectInfo ); + + self.assert_( hasattr( grp, "name" ) ); + self.assert_( hasattr( grp, "inherited" ) ); + self.assert_( hasattr( grp, "inherit" ) ); + self.assert_( hasattr( grp, "inheritable" ) ); + self.assert_( hasattr( grp, "add" ) ); + self.assert_( hasattr( grp, "remove" ) ); + self.assert_( hasattr( grp, "members" ) ); + + + diff --git a/mumble/urls.py b/mumble/urls.py new file mode 100644 index 0000000..776e5bb --- /dev/null +++ b/mumble/urls.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +""" + * Copyright б╘ 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +from django.conf.urls.defaults import patterns + +urlpatterns = patterns( + 'mumble.views', + ( r'djangousers', 'djangousers' ), + ( r'(?P\d+)/users', 'users' ), + + ( r'(?P\d+)/(?P\d+)/texture.png', 'showTexture' ), + + ( r'murmur/tree/(?P\d+)', 'mmng_tree' ), + ( r'mumbleviewer/(?P\d+).xml', 'mumbleviewer_tree_xml' ), + ( r'mumbleviewer/(?P\d+).json', 'mumbleviewer_tree_json'), + + ( r'mobile/(?P\d+)', 'mobile_show' ), + ( r'mobile/?$', 'mobile_mumbles' ), + + ( r'(?P\d+)', 'show' ), + ( r'$', 'mumbles' ), +) diff --git a/mumble/views.py b/mumble/views.py new file mode 100644 index 0000000..3574860 --- /dev/null +++ b/mumble/views.py @@ -0,0 +1,408 @@ +# -*- coding: utf-8 -*- + +""" + * Copyright б╘ 2009-2010, Michael "Svedrin" Ziegler + * + * Mumble-Django is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +""" + +import simplejson +from StringIO import StringIO +from PIL import Image + +from django.shortcuts import render_to_response, get_object_or_404, get_list_or_404 +from django.template import RequestContext +from django.http import Http404, HttpResponse, HttpResponseRedirect +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User +from django.conf import settings +from django.core.urlresolvers import reverse + +from django.contrib.auth import views as auth_views + +from models import Mumble, MumbleUser +from forms import MumbleForm, MumbleUserForm, MumbleUserPasswordForm +from forms import MumbleUserLinkForm, MumbleTextureForm, MumbleKickForm + + +def redir( request ): + """ Redirect to the servers list. """ + if request.META['HTTP_USER_AGENT'].startswith( 'BlackBerry' ) or \ + "Opera Mobi" in request.META['HTTP_USER_AGENT'] or \ + "Opera Mini" in request.META['HTTP_USER_AGENT'] or \ + "Windows CE" in request.META['HTTP_USER_AGENT'] or \ + "MIDP" in request.META['HTTP_USER_AGENT'] or \ + "Palm" in request.META['HTTP_USER_AGENT'] or \ + "NetFront" in request.META['HTTP_USER_AGENT'] or \ + "Nokia" in request.META['HTTP_USER_AGENT'] or \ + "Symbian" in request.META['HTTP_USER_AGENT'] or \ + "UP.Browser" in request.META['HTTP_USER_AGENT'] or \ + "UP.Link" in request.META['HTTP_USER_AGENT'] or \ + "WinWAP" in request.META['HTTP_USER_AGENT'] or \ + "Android" in request.META['HTTP_USER_AGENT'] or \ + "DoCoMo" in request.META['HTTP_USER_AGENT'] or \ + "KDDI-" in request.META['HTTP_USER_AGENT'] or \ + "Softbank" in request.META['HTTP_USER_AGENT'] or \ + "J-Phone" in request.META['HTTP_USER_AGENT'] or \ + "IEMobile" in request.META['HTTP_USER_AGENT'] or \ + "iPod" in request.META['HTTP_USER_AGENT'] or \ + "iPhone" in request.META['HTTP_USER_AGENT']: + return HttpResponseRedirect( reverse( mobile_mumbles ) ); + else: + return HttpResponseRedirect( reverse( mumbles ) ); + + +def mobile_mumbles( request ): + return mumbles( request, mobile=True ); + + +def mumbles( request, mobile=False ): + """ Display a list of all configured Mumble servers, or redirect if only one configured. """ + mms = Mumble.objects.all().order_by( "name" ); + + if len(mms) == 1: + return HttpResponseRedirect( reverse( + { False: show, True: mobile_show }[mobile], + kwargs={ 'server': mms[0].id, } + ) ); + + return render_to_response( + 'mumble/%s.html' % { False: 'list', True: 'mobile_list' }[mobile], + { 'MumbleObjects': mms, + 'MumbleActive': True, + }, + context_instance = RequestContext(request) + ); + + +def show( request, server ): + """ Display the channel list for the given Server ID. + + This includes not only the channel list itself, but indeed the user registration, + server admin and user texture form as well. The template then uses JavaScript + to display these forms integrated into the Channel viewer. + """ + srv = get_object_or_404( Mumble, id=server ); + if not srv.booted: + return render_to_response( + 'mumble/offline.html', + { 'DBaseObject': srv, + 'MumbleActive': True, + }, context_instance = RequestContext(request) ); + + isAdmin = srv.isUserAdmin( request.user ); + + # The tab to start on. + displayTab = 0; + + if isAdmin: + if request.method == 'POST' and "mode" in request.POST and request.POST['mode'] == 'admin': + adminform = MumbleForm( request.POST, instance=srv ); + if adminform.is_valid(): + adminform.save(); + return HttpResponseRedirect( reverse( show, kwargs={ 'server': int(server), } ) ); + else: + displayTab = 2; + else: + adminform = MumbleForm( instance=srv ); + else: + adminform = None; + + registered = False; + user = None; + + if request.user.is_authenticated(): + # Unregistered users may or may not need a password to register. + if settings.PROTECTED_MODE and srv.passwd: + unregged_user_form = MumbleUserPasswordForm; + # Unregistered users may or may not want to link an existing account + elif settings.ALLOW_ACCOUNT_LINKING: + unregged_user_form = MumbleUserLinkForm; + else: + unregged_user_form = MumbleUserForm; + + + if request.method == 'POST' and 'mode' in request.POST and request.POST['mode'] == 'reg': + try: + user = MumbleUser.objects.get( server=srv, owner=request.user ); + except MumbleUser.DoesNotExist: + regform = unregged_user_form( request.POST ); + regform.server = srv; + if regform.is_valid(): + model = regform.save( commit=False ); + model.owner = request.user; + model.server = srv; + # If we're linking accounts, the change is local only. + model.save( dontConfigureMurmur=( "linkacc" in regform.data ) ); + return HttpResponseRedirect( reverse( show, kwargs={ 'server': int(server), } ) ); + else: + displayTab = 1; + else: + regform = MumbleUserForm( request.POST, instance=user ); + regform.server = srv; + if regform.is_valid(): + regform.save(); + return HttpResponseRedirect( reverse( show, kwargs={ 'server': int(server), } ) ); + else: + displayTab = 1; + else: + try: + user = MumbleUser.objects.get( server=srv, owner=request.user ); + except MumbleUser.DoesNotExist: + regform = unregged_user_form(); + else: + regform = MumbleUserForm( instance=user ); + registered = True; + + if request.method == 'POST' and 'mode' in request.POST and request.POST['mode'] == 'texture' and registered: + textureform = MumbleTextureForm( request.POST, request.FILES ); + if textureform.is_valid(): + user.setTexture( Image.open( request.FILES['texturefile'] ) ); + return HttpResponseRedirect( reverse( show, kwargs={ 'server': int(server), } ) ); + else: + textureform = MumbleTextureForm(); + + else: + regform = None; + textureform = None; + + if isAdmin: + if request.method == 'POST' and 'mode' in request.POST and request.POST['mode'] == 'kick': + kickform = MumbleKickForm( request.POST ); + if kickform.is_valid(): + if kickform.cleaned_data["ban"]: + srv.banUser( kickform.cleaned_data['session'], kickform.cleaned_data['reason'] ); + srv.kickUser( kickform.cleaned_data['session'], kickform.cleaned_data['reason'] ); + + # ChannelTable is a somewhat misleading name, as it actually contains channels and players. + channelTable = []; + for cid in srv.channels: + if cid != 0 and srv.channels[cid].show: + channelTable.append( srv.channels[cid] ); + for pid in srv.players: + channelTable.append( srv.players[pid] ); + + show_url = reverse( show, kwargs={ 'server': srv.id } ); + login_url = reverse( auth_views.login ); + + return render_to_response( + 'mumble/mumble.html', + { + 'login_url': "%s?next=%s" % ( login_url, show_url ), + 'DBaseObject': srv, + 'ChannelTable': channelTable, + 'CurrentUserIsAdmin': isAdmin, + 'AdminForm': adminform, + 'RegForm': regform, + 'TextureForm': textureform, + 'Registered': registered, + 'DisplayTab': displayTab, + 'MumbleActive': True, + 'MumbleAccount':user, + }, + context_instance = RequestContext(request) + ); + +def mobile_show( request, server ): + """ Display the channel list for the given Server ID. """ + + srv = get_object_or_404( Mumble, id=server ); + + user = None; + if request.user.is_authenticated(): + try: + user = MumbleUser.objects.get( server=srv, owner=request.user ); + except MumbleUser.DoesNotExist: + pass; + + return render_to_response( + 'mumble/mobile_mumble.html', + { + 'DBaseObject': srv, + 'MumbleActive': True, + 'MumbleAccount':user, + }, + context_instance = RequestContext(request) + ); + + + + +def showTexture( request, server, userid ): + """ Pack the given user's texture into an HttpResponse. + + If userid is none, use the currently logged in User. + """ + + srv = get_object_or_404( Mumble, id=int(server) ); + user = get_object_or_404( MumbleUser, server=srv, id=int(userid) ); + + try: + img = user.getTexture(); + except ValueError: + raise Http404(); + else: + buf = StringIO(); + img.save( buf, "PNG" ); + return HttpResponse( buf.getvalue(), "image/png" ); + + +@login_required +def users( request, server ): + """ Create a list of MumbleUsers for a given server serialized as a JSON object. + + If the request has a "data" field, evaluate that and update the user records. + """ + + srv = get_object_or_404( Mumble, id=int(server) ); + + if "resync" in request.POST and request.POST['resync'] == "true": + srv.readUsersFromMurmur(); + + if not srv.isUserAdmin( request.user ): + return HttpResponse( + simplejson.dumps({ 'success': False, 'objects': [], 'errormsg': 'Access denied' }), + mimetype='text/javascript' + ); + + if request.method == 'POST': + data = simplejson.loads( request.POST['data'] ); + for record in data: + if record['id'] == -1: + if record['delete']: + continue; + mu = MumbleUser( server=srv ); + else: + mu = MumbleUser.objects.get( id=record['id'] ); + if record['delete']: + mu.delete(); + continue; + + mu.name = record['name']; + mu.password = record['password']; + if record['owner']: + mu.owner = User.objects.get( id=int(record['owner']) ); + mu.save(); + mu.aclAdmin = record['admin']; + + users = []; + for mu in srv.mumbleuser_set.all(): + owner = None; + if mu.owner is not None: + owner = mu.owner.id + + users.append( { + 'id': mu.id, + 'name': mu.name, + 'password': None, + 'owner': owner, + 'admin': mu.aclAdmin, + } ); + + return HttpResponse( + simplejson.dumps( { 'success': True, 'objects': users } ), + mimetype='text/javascript' + ); + + +@login_required +def djangousers( request ): + """ Return a list of all Django users' names and IDs. """ + + users = [ { 'uid': '', 'uname': '------' } ]; + for du in User.objects.all().order_by( 'username' ): + users.append( { + 'uid': du.id, + 'uname': unicode( du ), + } ); + + return HttpResponse( + simplejson.dumps( { 'success': True, 'objects': users } ), + mimetype='text/javascript' + ); + + +def mmng_tree( request, server ): + """ Return a JSON representation of the channel tree suitable for + Murmur Manager: + http://github.com/cheald/murmur-manager/tree/master/widget/ + + To make the client widget query this view, set the URL attribute + to "http:///mumble" + """ + + srv = get_object_or_404( Mumble, id=int(server) ); + + chanlist = [] + userlist = [] + + for chanid in srv.channels: + channel = srv.channels[chanid] + + if channel.parent is not None: + parent = channel.parent.chanid + else: + parent = -1 + + chanlist.append({ + "type": "channel", + "id": channel.chanid, + "name": channel.name, + "parent": parent, + "position": channel.position, + "state": channel.temporary and "temporary" or "permanent" + }) + + for sessionid in srv.players: + user = srv.players[sessionid] + userlist.append({ + "type": "player", + "name": user.name, + "channel": user.channel.chanid, + "mute": user.mute or user.selfMute or user.suppress, + "deaf": user.deaf or user.selfDeaf, + "online": user.onlinesecs, + "state": "online" + }) + + if "callback" in request.GET: + prefix = request.GET["callback"] + else: + prefix = "" + + return HttpResponse( + prefix + "(" + simplejson.dumps( { 'channels': chanlist, 'users': userlist } ) + ")", + mimetype='text/javascript' + ); + + +def mumbleviewer_tree_xml( request, server ): + """ Get the XML tree from the server and serialize it to the client. """ + from xml.etree.cElementTree import tostring as xml_to_string + srv = get_object_or_404( Mumble, id=int(server) ); + return HttpResponse( + xml_to_string( srv.asMvXml(), encoding='utf-8' ), + mimetype='text/xml' + ); + +def mumbleviewer_tree_json( request, server ): + """ Get the Dict from the server and serialize it as JSON to the client. """ + srv = get_object_or_404( Mumble, id=int(server) ); + + if "jsonp_callback" in request.GET: + prefix = request.GET["jsonp_callback"] + else: + prefix = "" + + return HttpResponse( + prefix + "(" + simplejson.dumps( srv.asMvJson() ) + ")", + mimetype='text/javascript' + ); From 9503dd12f691ec74a7357041529a3a19b7d1865c Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Tue, 23 Mar 2010 10:05:22 +0000 Subject: [PATCH 011/116] Removed django_cron reference --- urls.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/urls.py b/urls.py index f78e3e4..a9ce189 100644 --- a/urls.py +++ b/urls.py @@ -1,13 +1,11 @@ from django.conf.urls.defaults import * from django.contrib import admin from django.contrib.auth.views import login -import django_cron import settings from registration.views import register from registration.forms import RegistrationFormUniqueEmail -django_cron.autodiscover() admin.autodiscover() urlpatterns = patterns('', From 05f5df397c2ea6089cad894c867432f74e6b77fe Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Tue, 23 Mar 2010 10:17:00 +0000 Subject: [PATCH 012/116] Now links users to mumble, and check_user works with virtual servers --- settings.py | 4 ++-- sso/services/mumble/__init__.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/settings.py b/settings.py index 249751b..5d0778d 100644 --- a/settings.py +++ b/settings.py @@ -112,9 +112,9 @@ JABBER_AUTH_PASSWD = 'pepperllama34' ### Mumble Service Settings DEFAULT_CONN = 'Meta:tcp -h 127.0.0.1 -p 6502' -MUMBLE_DEFAULT_PORT = 64738 +MUMBLE_DEFAULT_PORT = 64740 SLICE = 'Murmur.ice' -MUMBLE_SERVER_ID = 1 +MUMBLE_SERVER_ID = 2 ### Wiki Service Settings diff --git a/sso/services/mumble/__init__.py b/sso/services/mumble/__init__.py index 7cbfc33..71b42d4 100644 --- a/sso/services/mumble/__init__.py +++ b/sso/services/mumble/__init__.py @@ -18,13 +18,17 @@ class MumbleService(BaseService): mumbleuser.name = username mumbleuser.password = password mumbleuser.server = self._get_server() + + if 'user' in kwargs: + mumbleuser.user = kwargs['user'] + mumbleuser.save() return mumbleuser.name def check_user(self, username): """ Check if the username exists """ try: - mumbleuser = MumbleUser.objects.get(name=username) + mumbleuser = MumbleUser.objects.get(name=username, server=self._get_server()) except MumbleUser.DoesNotExist: return False else: From 596cc0bab401ef2cf49f2407a01322a3b76d1f37 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Tue, 23 Mar 2010 10:24:00 +0000 Subject: [PATCH 013/116] Fixed del_user and added enable/disable functions to mumble service --- sso/services/mumble/__init__.py | 16 +++++++++++++--- sso/views.py | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/sso/services/mumble/__init__.py b/sso/services/mumble/__init__.py index 71b42d4..bc18ef6 100644 --- a/sso/services/mumble/__init__.py +++ b/sso/services/mumble/__init__.py @@ -36,16 +36,26 @@ class MumbleService(BaseService): def delete_user(self, uid): """ Delete a user by uid """ - mumbleuser = MumbleUser.objects.get(name=uid) + mumbleuser = MumbleUser.objects.get(name=uid, server=self._get_server()) mumbleuser.delete() def disable_user(self, uid): """ Disable a user by uid """ - pass + try: + mumbleuser = MumbleUser.objects.get(name=uid, server=self._get_server()) + except MumbleUser.DoesNotExist: + return False + mumbleuser.password = "" + mumbleuser.save() def enable_user(self, uid, password): """ Enable a user by uid """ - pass + try: + mumbleuser = MumbleUser.objects.get(name=uid, server=self._get_server()) + except MumbleUser.DoesNotExist: + return False + mumbleuser.password = password + mumbleuser.save() def login(uid): """ Login the user and provide cookies back """ diff --git a/sso/views.py b/sso/views.py index 27b8d21..894023e 100644 --- a/sso/views.py +++ b/sso/views.py @@ -133,7 +133,7 @@ def service_add(request): else: error = None - return render_to_response('sso/serviceaccount/created.html', { 'account': acc, 'error': error }) + return render_to_response('sso/serviceaccount/created.html', { 'account': acc, 'error': error }, context_instance=RequestContext(request)) else: #defaults = { 'username': request.user.username, 'password': request.user.get_profile().default_service_passwd } From fa4a084292cc1c323868e94e87937c899dcf8b3b Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Tue, 23 Mar 2010 10:49:12 +0000 Subject: [PATCH 014/116] Added delete confirmation for services. Generally cleanup of service views --- sso/views.py | 31 +++++++++++-------- templates/sso/serviceaccount/created.html | 10 +++--- .../sso/serviceaccount/deleteconfirm.html | 16 ++++++++++ 3 files changed, 39 insertions(+), 18 deletions(-) create mode 100644 templates/sso/serviceaccount/deleteconfirm.html diff --git a/sso/views.py b/sso/views.py index 894023e..360a4fe 100644 --- a/sso/views.py +++ b/sso/views.py @@ -20,7 +20,7 @@ from reddit.models import RedditAccount import settings def index(request): - return render_to_response('sso/index.html') + return render_to_response('sso/index.html', context_instance=RequestContext(request)) @login_required def profile(request): @@ -133,10 +133,8 @@ def service_add(request): else: error = None - return render_to_response('sso/serviceaccount/created.html', { 'account': acc, 'error': error }, context_instance=RequestContext(request)) + return render_to_response('sso/serviceaccount/created.html', locals(), context_instance=RequestContext(request)) else: - #defaults = { 'username': request.user.username, 'password': request.user.get_profile().default_service_passwd } - availserv = Service.objects.filter(groups__in=request.user.groups.all()).exclude(id__in=ServiceAccount.objects.filter(user=request.user).values('service')) if len(availserv) == 0: return render_to_response('sso/serviceaccount/noneavailable.html', locals(), context_instance=RequestContext(request)) @@ -148,15 +146,22 @@ def service_add(request): @login_required def service_del(request, serviceid=0): if serviceid > 0 : - try: acc = ServiceAccount.objects.get(id=serviceid) except ServiceAccount.DoesNotExist: return HttpResponseRedirect(reverse('sso.views.profile')) - if acc.user == request.user: - acc.delete() - request.user.message_set.create(message="Service account successfully deleted.") + if not acc.user == request.user: + return HttpResponseRedirect(reverse('sso.views.profile')) + + if request.method == 'POST': + print request.POST + if 'confirm-delete' in request.POST: + acc.delete() + request.user.message_set.create(message="Service account successfully deleted.") + else: + return render_to_response('sso/serviceaccount/deleteconfirm.html', locals(), context_instance=RequestContext(request)) + return HttpResponseRedirect(reverse('sso.views.profile')) @login_required @@ -169,7 +174,7 @@ def service_reset(request, serviceid=0, accept=0): if acc.user == request.user: if not accept: - return render_to_response('sso/serviceaccount/reset.html', locals()) + return render_to_response('sso/serviceaccount/reset.html', locals(), context_instance=RequestContext(request)) passwd = hashlib.sha1('%s%s%s' % (acc.service_uid, settings.SECRET_KEY, random.randint(0, 2147483647))).hexdigest() @@ -201,7 +206,7 @@ def reddit_add(request): defaults = { 'username': request.user.username, } form = RedditAccountForm(defaults) # An unbound form - return render_to_response('sso/redditaccount.html', locals()) + return render_to_response('sso/redditaccount.html', locals(), context_instance=RequestContext(request)) @login_required def reddit_del(request, redditid=0): @@ -230,9 +235,9 @@ def user_view(request, user=None): if form.is_valid(): user = form.cleaned_data['username'] else: - return render_to_response('sso/userlookup.html', locals()) + return render_to_response('sso/userlookup.html', locals(), context_instance=RequestContext(request)) else: - return render_to_response('sso/userlookup.html', locals()) + return render_to_response('sso/userlookup.html', locals(), context_instance=RequestContext(request)) is_admin = request.user.is_staff @@ -263,4 +268,4 @@ def user_view(request, user=None): except EVEAccount.DoesNotExist: eveaccounts = None - return render_to_response('sso/user.html', locals()) + return render_to_response('sso/user.html', locals(), context_instance=RequestContext(request)) diff --git a/templates/sso/serviceaccount/created.html b/templates/sso/serviceaccount/created.html index a69c94b..8e00c80 100644 --- a/templates/sso/serviceaccount/created.html +++ b/templates/sso/serviceaccount/created.html @@ -9,13 +9,13 @@ this is incorrect please raise a bug on the tracker. {% else %} -

Your account on {{ account.service }} has been created. Your login details are as follows:

+

Your account on {{ acc.service }} has been created. Your login details are as follows:

- - - - + + + +
Service:{{ account.service.name }}
Service URL:{{ account.service.url }}
Username:{{ account.service_uid }}
Password:{{ account.password }}
Service:{{ acc.service.name }}
Service URL:{{ acc.service.url }}
Username:{{ acc.service_uid }}
Password:{{ acc.password }}

Warning: You password is random, please either note it down or once logged into the service change it to something you diff --git a/templates/sso/serviceaccount/deleteconfirm.html b/templates/sso/serviceaccount/deleteconfirm.html new file mode 100644 index 0000000..9a9bce7 --- /dev/null +++ b/templates/sso/serviceaccount/deleteconfirm.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block title %}External Service Account{% endblock %} + +{% block content %} +

Warning: This will delete your account on {{ acc.service.name }}, you will no longer be able to login and in some situtations unable to +create a new account until fixed by a Sysop. If you are having issues logging in then please use the password reset function first!

+ +

If you are sure, then please click confirm

+ +
+ + +
+{% endblock %} + From fe54116057c7d0da3184867e13764da731e437d2 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Tue, 23 Mar 2010 12:59:16 +0000 Subject: [PATCH 015/116] Massive reworking on the user lookup form to support various other lookup types (Character/Reddit ID) --- sso/forms.py | 27 ++++-- sso/urls.py | 4 +- sso/views.py | 99 ++++++++++++---------- templates/sso/lookup/lookuplist.html | 15 ++++ templates/sso/{ => lookup}/user.html | 0 templates/sso/{ => lookup}/userlookup.html | 0 6 files changed, 93 insertions(+), 52 deletions(-) create mode 100644 templates/sso/lookup/lookuplist.html rename templates/sso/{ => lookup}/user.html (100%) rename templates/sso/{ => lookup}/userlookup.html (100%) diff --git a/sso/forms.py b/sso/forms.py index b79829d..eaac5a4 100644 --- a/sso/forms.py +++ b/sso/forms.py @@ -17,7 +17,6 @@ class EveAPIForm(forms.Form): def clean(self): if not len(self.cleaned_data['api_key']) == 64: raise forms.ValidationError("API Key provided is invalid (Not 64 characters long)") - try: eaccount = EVEAccount.objects.get(api_user_id=self.cleaned_data['user_id']) except EVEAccount.DoesNotExist: @@ -61,11 +60,29 @@ class RedditAccountForm(forms.Form): class UserLookupForm(forms.Form): """ User Lookup Form """ + choices = [ (1, "Auth Username"), + (2, "Character"), + (3, "Reddit ID") ] + + type = forms.ChoiceField(label = u'Search type', choices = choices) username = forms.CharField(label = u'User ID', max_length=64) def clean(self): - try: - acc = User.objects.get(username=self.cleaned_data['username']) - except User.DoesNotExist: - raise forms.ValidationError("User doesn't exist") + + if self.cleaned_data['type'] == 1: + try: + acc = User.objects.filter(username=self.cleaned_data['username']) + except User.DoesNotExist: + raise forms.ValidationError("User doesn't exist") + elif self.cleaned_data['type'] == 2: + try: + acc = EVEPlayerCharacter.filter(name=self.cleaned_data['username']) + except User.DoesNotExist: + raise forms.ValidationError("Character doesn't exist") + elif self.cleaned_data['type'] == 3: + try: + acc = RedditAccount.filter(name=self.cleaned_data['username']) + except User.DoesNotExist: + raise forms.ValidationError("Account doesn't exist") + return self.cleaned_data diff --git a/sso/urls.py b/sso/urls.py index ceab6b6..c978f21 100644 --- a/sso/urls.py +++ b/sso/urls.py @@ -18,6 +18,6 @@ urlpatterns = patterns('', (r'^profile/del/reddit/(?P\d+)/$', views.reddit_del), (r'^profile/characters$', views.characters), (r'^profile/characters/(?P.*)/$', views.characters), - (r'^users/(?P.*)/$', views.user_view), - (r'^users/$', views.user_view), + (r'^users/(?P.*)/$', views.user_view), + (r'^users/$', views.user_lookup), ) diff --git a/sso/views.py b/sso/views.py index 360a4fe..35e8768 100644 --- a/sso/views.py +++ b/sso/views.py @@ -155,7 +155,6 @@ def service_del(request, serviceid=0): return HttpResponseRedirect(reverse('sso.views.profile')) if request.method == 'POST': - print request.POST if 'confirm-delete' in request.POST: acc.delete() request.user.message_set.create(message="Service account successfully deleted.") @@ -190,14 +189,11 @@ def service_reset(request, serviceid=0, accept=0): def reddit_add(request): if request.method == 'POST': form = RedditAccountForm(request.POST) - if form.is_valid(): - + if form.is_valid(): acc = RedditAccount() - acc.user = request.user acc.username = form.cleaned_data['username'] acc.api_update() - acc.save() request.user.message_set.create(message="Reddit account %s successfully added." % acc.username) @@ -225,47 +221,60 @@ def reddit_del(request, redditid=0): @login_required -def user_view(request, user=None): +def user_view(request, username=None): + if username: + user = User.objects.get(username=username) + else: + return HttpResponseRedirect(reverse('sso.views.user_lookup')) + + profile = user.get_profile() + is_admin = request.user.is_staff + if is_admin: + try: + services = ServiceAccount.objects.filter(user=user).all() + except ServiceAccount.DoesNotExist: + services = None + try: + reddits = RedditAccount.objects.filter(user=user).all() + except ServiceAccount.DoesNotExist: + reddits = None + try: + eveaccounts = EVEAccount.objects.filter(user=user).all() + characters = [] + for acc in eveaccounts: + chars = acc.characters.all() + for char in chars: + characters.append({'name': char.name, 'corp': char.corporation.name}) + except EVEAccount.DoesNotExist: + eveaccounts = None + + return render_to_response('sso/lookup/user.html', locals(), context_instance=RequestContext(request)) + +def user_lookup(request): form = UserLookupForm() - if user: - user = user - elif request.method == 'POST': - form = UserLookupForm(request.POST) + if request.method == 'POST': + form = UserLookupForm(request.POST) if form.is_valid(): - user = form.cleaned_data['username'] - else: - return render_to_response('sso/userlookup.html', locals(), context_instance=RequestContext(request)) - else: - return render_to_response('sso/userlookup.html', locals(), context_instance=RequestContext(request)) + users = None + uids = [] + if form.cleaned_data['type'] == '1': + users = User.objects.filter(username=form.cleaned_data['username']) + elif form.cleaned_data['type'] == '2': + uid = EVEAccount.objects.filter(characters__name=form.cleaned_data['username']).values('user') + for u in uid: uids.append(u['user']) + users = User.objects.filter(id__in=uids) + elif form.cleaned_data['type'] == '3': + uid = RedditAccount.objects.filter(username=form.cleaned_data['username']).values('user') + for u in uid: uids.append(u['user']) + users = User.objects.filter(id__in=uids) + else: + request.user.message_set.create(message="Error parsing form, Type: %s, Value: %s" % (form.cleaned_data['type'], form.cleaned_data['username'])) + return HttpResponseRedirect(reverse('sso.views.user_lookup')) - is_admin = request.user.is_staff - - user = User.objects.get(username=user) - profile = user.get_profile() - - if is_admin: - try: - services = ServiceAccount.objects.filter(user=user).all() - except ServiceAccount.DoesNotExist: - services = None - - try: - reddits = RedditAccount.objects.filter(user=user).all() - except ServiceAccount.DoesNotExist: - reddits = None - - try: - eveaccounts = EVEAccount.objects.filter(user=user).all() - - characters = [] - - for acc in eveaccounts: - chars = acc.characters.all() - for char in chars: - characters.append({'name': char.name, 'corp': char.corporation.name}) - - except EVEAccount.DoesNotExist: - eveaccounts = None - - return render_to_response('sso/user.html', locals(), context_instance=RequestContext(request)) + if users and len(users) == 1: + return HttpResponseRedirect(reverse(user_view, kwargs={'username': users[0].username})) + elif users and len(users) > 1: + render_to_response('sso/lookup/lookuplist.html', locals(), context_instance=RequestContext(request)) + + return render_to_response('sso/lookup/userlookup.html', locals(), context_instance=RequestContext(request)) diff --git a/templates/sso/lookup/lookuplist.html b/templates/sso/lookup/lookuplist.html new file mode 100644 index 0000000..ce8e603 --- /dev/null +++ b/templates/sso/lookup/lookuplist.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} + +{% block title %}User Lookup List{% endblock %} + +{% block content %} + +

More than one user was found matching your criteria:

+ + + +{% endblock %} diff --git a/templates/sso/user.html b/templates/sso/lookup/user.html similarity index 100% rename from templates/sso/user.html rename to templates/sso/lookup/user.html diff --git a/templates/sso/userlookup.html b/templates/sso/lookup/userlookup.html similarity index 100% rename from templates/sso/userlookup.html rename to templates/sso/lookup/userlookup.html From ebc88200e719185f35c3486eb49e08d78ad6156a Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Tue, 23 Mar 2010 13:15:18 +0000 Subject: [PATCH 016/116] Added some support to close database connections on object deletion. --- sso/services/miningbuddy/__init__.py | 4 ++++ sso/services/wiki/__init__.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/sso/services/miningbuddy/__init__.py b/sso/services/miningbuddy/__init__.py index 86784bd..e8647a6 100644 --- a/sso/services/miningbuddy/__init__.py +++ b/sso/services/miningbuddy/__init__.py @@ -38,6 +38,10 @@ class MiningBuddyService(BaseService): self._dbcursor = self._db.cursor() + def __del__(self): + self._db.close() + self._db = None + def _gen_salt(self): return settings.MINING_SALT diff --git a/sso/services/wiki/__init__.py b/sso/services/wiki/__init__.py index 2e66c0e..2da08b0 100644 --- a/sso/services/wiki/__init__.py +++ b/sso/services/wiki/__init__.py @@ -38,6 +38,10 @@ class MediawikiService(BaseService): self._dbcursor = self._db.cursor() + def __del__(self): + self._db.close() + self._db = None + def _gen_salt(self): return "%x" % random.randint(0, 2147483647) From 83ca5366532591f378efdeb09e0d848b8f32aa68 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Tue, 23 Mar 2010 13:21:50 +0000 Subject: [PATCH 017/116] Now allows partial search on names --- sso/views.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/sso/views.py b/sso/views.py index 35e8768..9ae060d 100644 --- a/sso/views.py +++ b/sso/views.py @@ -259,13 +259,13 @@ def user_lookup(request): users = None uids = [] if form.cleaned_data['type'] == '1': - users = User.objects.filter(username=form.cleaned_data['username']) + users = User.objects.filter(username__contains=form.cleaned_data['username']) elif form.cleaned_data['type'] == '2': - uid = EVEAccount.objects.filter(characters__name=form.cleaned_data['username']).values('user') + uid = EVEAccount.objects.filter(characters__name__contains=form.cleaned_data['username']).values('user') for u in uid: uids.append(u['user']) users = User.objects.filter(id__in=uids) elif form.cleaned_data['type'] == '3': - uid = RedditAccount.objects.filter(username=form.cleaned_data['username']).values('user') + uid = RedditAccount.objects.filter(username__contains=form.cleaned_data['username']).values('user') for u in uid: uids.append(u['user']) users = User.objects.filter(id__in=uids) else: @@ -276,5 +276,8 @@ def user_lookup(request): return HttpResponseRedirect(reverse(user_view, kwargs={'username': users[0].username})) elif users and len(users) > 1: render_to_response('sso/lookup/lookuplist.html', locals(), context_instance=RequestContext(request)) + else: + request.user.message_set.create(message="No results found") + return HttpResponseRedirect(reverse('sso.views.user_lookup')) return render_to_response('sso/lookup/userlookup.html', locals(), context_instance=RequestContext(request)) From 84191f24e8ed343d01cd5c8bb79b0a87b5a6a47d Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Tue, 23 Mar 2010 13:37:31 +0000 Subject: [PATCH 018/116] More error checking on Jabber, also presents error message to user if service addition fails --- sso/models.py | 5 +++++ sso/services/jabber/__init__.py | 1 - sso/services/jabber/xmppclient.py | 28 ++++++++++++++++++++++------ sso/views.py | 2 ++ 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/sso/models.py b/sso/models.py index ccb0376..d7481bf 100644 --- a/sso/models.py +++ b/sso/models.py @@ -17,6 +17,9 @@ class CorporateOnlyService(Exception): class ExistingUser(Exception): pass +class ServiceError(Exception): + pass + ## Models class SSOUser(models.Model): @@ -129,6 +132,8 @@ class ServiceAccount(models.Model): reddit = RedditAccount.objects.filter(user=self.user) self.service_uid = api.add_user(self.username, self.password, user=self.user, character=self.character, eveapi=eveapi, reddit=reddit) + if not self.service_uid: + raise ServiceError('Error occured while trying to create the Service Account, please try again later') else: raise ExistingUser('Username %s has already been took' % self.username) else: diff --git a/sso/services/jabber/__init__.py b/sso/services/jabber/__init__.py index 0566f4c..41f56f5 100644 --- a/sso/services/jabber/__init__.py +++ b/sso/services/jabber/__init__.py @@ -16,7 +16,6 @@ class JabberService(BaseService): if settings.JABBER_METHOD == "xmpp": self.method = "xmpp" self.jabberadmin = JabberAdmin(settings.JABBER_SERVER, settings.JABBER_AUTH_USER, settings.JABBER_AUTH_PASSWD) - self.jabberadmin.connect() else: self.method = "cmd" self.ejctl = eJabberdCtl(sudo=settings.JABBER_SUDO) diff --git a/sso/services/jabber/xmppclient.py b/sso/services/jabber/xmppclient.py index ebb9c01..9828d10 100644 --- a/sso/services/jabber/xmppclient.py +++ b/sso/services/jabber/xmppclient.py @@ -22,14 +22,14 @@ class JabberAdmin(): self._client.disconnect() def connect(self): + if not hasattr(self, '_client'): + client = xmpp.Client(self.jid.getDomain(), debug=[]) - client = xmpp.Client(self.jid.getDomain(), debug=[]) + client.connect(server=('dredd.it', 5222)) + client.auth(self.username, self.password) + client.sendInitPresence() - client.connect(server=('dredd.it', 5222)) - client.auth(self.username, self.password) - client.sendInitPresence() - - self._client = client + self._client = client def _construct_iq_req(self, xmlns, node): n = xmpp.Node('command', attrs={'xmlns': xmlns, 'node': node}) @@ -49,6 +49,10 @@ class JabberAdmin(): def adduser(self, username, password): + try: + self.connect() + except: + return False # Send request and get the Session ID resp = self._client.SendAndWaitForResponse(self._construct_iq_req('http://jabber.org/protocol/commands', 'http://jabber.org/protocol/admin#add-user')) sessionid = resp.getTagAttr('command','sessionid') @@ -70,6 +74,10 @@ class JabberAdmin(): def deluser(self, username): + try: + self.connect() + except: + return False # Send request and get the Session ID resp = self._client.SendAndWaitForResponse(self._construct_iq_req('http://jabber.org/protocol/commands', 'http://jabber.org/protocol/admin#delete-user')) sessionid = resp.getTagAttr('command','sessionid') @@ -88,6 +96,10 @@ class JabberAdmin(): return False def resetpassword(self, username, password): + try: + self.connect() + except: + return False # Send request and get the Session ID resp = self._client.SendAndWaitForResponse(self._construct_iq_req('http://jabber.org/protocol/commands', 'http://jabber.org/protocol/admin#change-user-password')) sessionid = resp.getTagAttr('command','sessionid') @@ -108,6 +120,10 @@ class JabberAdmin(): def checkuser(self, username): + try: + self.connect() + except: + return False # Send request and get the Session ID resp = self._client.SendAndWaitForResponse(self._construct_iq_req('http://jabber.org/protocol/commands', 'http://jabber.org/protocol/admin#get-user-password')) sessionid = resp.getTagAttr('command','sessionid') diff --git a/sso/views.py b/sso/views.py index 9ae060d..27fc0ea 100644 --- a/sso/views.py +++ b/sso/views.py @@ -130,6 +130,8 @@ def service_add(request): acc.save() except ExistingUser: error = "User by this name already exists, your account has not been created" + except ServiceError: + error = "A error occured while trying to create the Service Account, please try again later" else: error = None From d5256549179c405c935bb7cd3376fd9ac0bfc51a Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Tue, 23 Mar 2010 14:44:32 +0000 Subject: [PATCH 019/116] Added mumble evolutions --- mumble/evolutions/__init__.py | 0 mumble/evolutions/mumble-db-update-1.py | 10 ++++++++++ 2 files changed, 10 insertions(+) create mode 100644 mumble/evolutions/__init__.py create mode 100644 mumble/evolutions/mumble-db-update-1.py diff --git a/mumble/evolutions/__init__.py b/mumble/evolutions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mumble/evolutions/mumble-db-update-1.py b/mumble/evolutions/mumble-db-update-1.py new file mode 100644 index 0000000..7ad3270 --- /dev/null +++ b/mumble/evolutions/mumble-db-update-1.py @@ -0,0 +1,10 @@ +from django_evolution.mutations import * +from django.db import models + +MUTATIONS = [ + AddField('Mumble', 'display', models.CharField, initial='', max_length=200), + AddField('Mumble', 'server', models.ForeignKey, initial=<>, related_model='mumble.MumbleServer'), + DeleteField('Mumble', 'dbus'), + ChangeField('Mumble', 'port', initial=None, null=True) +] + From 72ac2ecb6f60a257aa6e59be86bcd3801f3503d8 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Tue, 23 Mar 2010 14:48:55 +0000 Subject: [PATCH 020/116] Added evolution to the sequence list --- mumble/evolutions/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mumble/evolutions/__init__.py b/mumble/evolutions/__init__.py index e69de29..cc4d13a 100644 --- a/mumble/evolutions/__init__.py +++ b/mumble/evolutions/__init__.py @@ -0,0 +1 @@ +SEQUENCE = ['mumble-db-update-1'] From 1a6fd720d3e75603943480d80739d1c32fab09b5 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Tue, 23 Mar 2010 14:50:50 +0000 Subject: [PATCH 021/116] Fixed evolution --- mumble/evolutions/mumble-db-update-1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mumble/evolutions/mumble-db-update-1.py b/mumble/evolutions/mumble-db-update-1.py index 7ad3270..0b8eae6 100644 --- a/mumble/evolutions/mumble-db-update-1.py +++ b/mumble/evolutions/mumble-db-update-1.py @@ -3,7 +3,7 @@ from django.db import models MUTATIONS = [ AddField('Mumble', 'display', models.CharField, initial='', max_length=200), - AddField('Mumble', 'server', models.ForeignKey, initial=<>, related_model='mumble.MumbleServer'), + AddField('Mumble', 'server', models.ForeignKey, initial='<>', related_model='mumble.MumbleServer'), DeleteField('Mumble', 'dbus'), ChangeField('Mumble', 'port', initial=None, null=True) ] From 1e2b18f1d0fe3484ece539dc6ddd445b15bfd537 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Tue, 23 Mar 2010 16:05:02 +0000 Subject: [PATCH 022/116] Fix SQL error on delete user --- sso/services/wiki/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sso/services/wiki/__init__.py b/sso/services/wiki/__init__.py index 2da08b0..e65ce7a 100644 --- a/sso/services/wiki/__init__.py +++ b/sso/services/wiki/__init__.py @@ -21,7 +21,7 @@ class MediawikiService(BaseService): SQL_CHECK_USER = r"SELECT user_name from user WHERE user_name = %s" SQL_DEL_REV = r"UPDATE revision SET rev_user = (SELECT user_id FROM user WHERE user_name = 'DeletedUser'), rev_user_text = 'DeletedUser' WHERE rev_user = (SELECT user_id FROM user WHERE user_name = %s)" - SQL_DEL_USER = r"DELETE FROM USER WHERE user_name = %s" + SQL_DEL_USER = r"DELETE FROM user WHERE user_name = %s" def __init__(self): From 81804cb409d2e1862f4f373b4e24d192520134f2 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Tue, 23 Mar 2010 21:00:33 +0000 Subject: [PATCH 023/116] Permission updates now disables accounts instead of deleting them, --- sso/cron.py | 18 ++++++++++++++---- sso/views.py | 8 +++----- templates/sso/profile.html | 9 ++++++--- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/sso/cron.py b/sso/cron.py index d686cfc..92597f1 100644 --- a/sso/cron.py +++ b/sso/cron.py @@ -23,18 +23,28 @@ class RemoveInvalidUsers(): # For each user, update access list based on Corp details user.get_profile().update_access() - # Check each service account and delete access if they're not allowed + # Check each service account and disable access if they're not allowed for servacc in ServiceAccount.objects.filter(user=user): if not (set(user.groups.all()) & set(servacc.service.groups.all())): self._logger.info("User %s is not in allowed group for %s, deleting account" % (user.username, servacc.service)) - servacc.delete() + servacc.active = 0 + servacc.save() + servacc.user.message_set.create(message="Your %s account has been disabled due to lack of permissions. If this is incorrect, check your API keys." % (servacc.service)) pass + else: + if not servacc.active: + self._logger.info("User % is now in a allowed group for %s, enabling account" % (user.username, servacc.service)) + servacc.active = 1 + servacc.save() + servacc.user.message_set.create(message="Your %s account has been re-enabled, you may need to reset your password to access this service again." % (servacc.service)) + pass # For users set to not active, delete all accounts if not user.is_active: - print "User %s is inactive, deleting related service accounts" % user.username + print "User %s is inactive, disabling related service accounts" % user.username for servacc in ServiceAccount.objects.filter(user=user): - servacc.delete() + servacc.active = 0 + servacc.save() pass diff --git a/sso/views.py b/sso/views.py index 27fc0ea..c1ea8d1 100644 --- a/sso/views.py +++ b/sso/views.py @@ -119,9 +119,7 @@ def service_add(request): if form.is_valid(): acc = ServiceAccount() - acc.user = request.user - acc.service = form.cleaned_data['service'] acc.character = form.cleaned_data['character'] acc.password = hashlib.sha1('%s%s%s' % (form.cleaned_data['character'].name, settings.SECRET_KEY, random.randint(0, 2147483647))).hexdigest() @@ -173,6 +171,9 @@ def service_reset(request, serviceid=0, accept=0): except ServiceAccount.DoesNotExist: return HttpResponseRedirect(reverse('sso.views.profile')) + if not acc.active: + return HttpResponseRedirect(reverse('sso.views.profile')) + if acc.user == request.user: if not accept: return render_to_response('sso/serviceaccount/reset.html', locals(), context_instance=RequestContext(request)) @@ -186,7 +187,6 @@ def service_reset(request, serviceid=0, accept=0): return HttpResponseRedirect(reverse('sso.views.profile')) - @login_required def reddit_add(request): if request.method == 'POST': @@ -220,8 +220,6 @@ def reddit_del(request, redditid=0): return HttpResponseRedirect(reverse('sso.views.profile')) - - @login_required def user_view(request, username=None): if username: diff --git a/templates/sso/profile.html b/templates/sso/profile.html index 1cf5518..6af6301 100644 --- a/templates/sso/profile.html +++ b/templates/sso/profile.html @@ -25,12 +25,15 @@ create a login for a service click the Add Service link

{{ acc.service }} {{ acc.service_uid }} {{ acc.service.url }} - {{ acc.active }} - Delete / Reset + {% if acc.active %}Yes{% else %}No{% endif %} + Delete + {% if acc.active %} +  / Reset {% if acc.service.provide_login %}  / Login {% endif %} - + {% endif %} + {% endfor %} From 3e5317355ff6acd3e858d4bbc21c393b5a4cb64b Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Tue, 23 Mar 2010 21:04:07 +0000 Subject: [PATCH 024/116] Moved username cleanup to new serviceaccount creation, stopping random error on saving --- sso/models.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sso/models.py b/sso/models.py index d7481bf..55c079a 100644 --- a/sso/models.py +++ b/sso/models.py @@ -110,19 +110,19 @@ class ServiceAccount(models.Model): def save(self): """ Override default save to setup accounts as needed """ - # Force username to be the same as their selected character - # Fix unicode first of all - name = unicodedata.normalize('NFKD', self.character.name).encode('ASCII', 'ignore') - - # Remove spaces and non-acceptable characters - self.username = re.sub('[^a-zA-Z0-9_-]+', '', name) - # Grab the API class api = self.service.api_class if not self.service_uid: # Create a account if we've not got a UID if self.active: + # Force username to be the same as their selected character + # Fix unicode first of all + name = unicodedata.normalize('NFKD', self.character.name).encode('ASCII', 'ignore') + + # Remove spaces and non-acceptable characters + self.username = re.sub('[^a-zA-Z0-9_-]+', '', name) + if not api.check_user(self.username): eveapi = None for eacc in EVEAccount.objects.filter(user=self.user): From 576b3c3bc225205ba11d6192a32f5f3290e2d5a0 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Wed, 24 Mar 2010 09:34:58 +0000 Subject: [PATCH 025/116] if the account is already disabled, skip it --- sso/cron.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sso/cron.py b/sso/cron.py index 92597f1..8aa496d 100644 --- a/sso/cron.py +++ b/sso/cron.py @@ -26,10 +26,11 @@ class RemoveInvalidUsers(): # Check each service account and disable access if they're not allowed for servacc in ServiceAccount.objects.filter(user=user): if not (set(user.groups.all()) & set(servacc.service.groups.all())): - self._logger.info("User %s is not in allowed group for %s, deleting account" % (user.username, servacc.service)) - servacc.active = 0 - servacc.save() - servacc.user.message_set.create(message="Your %s account has been disabled due to lack of permissions. If this is incorrect, check your API keys." % (servacc.service)) + if servacc.active: + self._logger.info("User %s is not in allowed group for %s, deleting account" % (user.username, servacc.service)) + servacc.active = 0 + servacc.save() + servacc.user.message_set.create(message="Your %s account has been disabled due to lack of permissions. If this is incorrect, check your API keys." % (servacc.service)) pass else: if not servacc.active: From 15f9dc972ac8fe6590e654f8bad5e992e2e55f7b Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Wed, 24 Mar 2010 10:25:08 +0000 Subject: [PATCH 026/116] Now checks for errors updating the reddit user's api, fixes #38 --- sso/views.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sso/views.py b/sso/views.py index c1ea8d1..474d7ff 100644 --- a/sso/views.py +++ b/sso/views.py @@ -195,7 +195,11 @@ def reddit_add(request): acc = RedditAccount() acc.user = request.user acc.username = form.cleaned_data['username'] - acc.api_update() + try: + acc.api_update() + except RedditAccount.DoesNotExist: + request.user.message_set.create(message="Error, user %s does not exist on Reddit" % acc.username ) + return render_to_response('sso/redditaccount.html', locals(), context_instance=RequestContext(request)) acc.save() request.user.message_set.create(message="Reddit account %s successfully added." % acc.username) From 423856a19a6b3eb2b2ae2815a4ce3263815c7451 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Wed, 24 Mar 2010 10:30:08 +0000 Subject: [PATCH 027/116] Added start script --- start.sh | 3 +++ 1 file changed, 3 insertions(+) create mode 100755 start.sh diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..435c0a9 --- /dev/null +++ b/start.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +/usr/bin/python ./manage.py runfcgi method=threaded host=127.0.0.1 port=9981 daemonize=true pidfile=./auth.pid From 6220f1f07fd0255c35216e238bd13ef9079cd83e Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Wed, 24 Mar 2010 11:08:02 +0000 Subject: [PATCH 028/116] Enable disabling of users on Jabber via xmpp --- sso/services/jabber/__init__.py | 4 ++-- sso/services/jabber/xmppclient.py | 33 +++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/sso/services/jabber/__init__.py b/sso/services/jabber/__init__.py index 41f56f5..6b3314f 100644 --- a/sso/services/jabber/__init__.py +++ b/sso/services/jabber/__init__.py @@ -49,7 +49,7 @@ class JabberService(BaseService): def disable_user(self, uid): """ Disable a user """ if self.method == "xmpp": - return False + return self.jabberadmin.disableuser(uid) else: username, server = uid.split("@") return self.ejctl.ban_user(server, username) @@ -57,7 +57,7 @@ class JabberService(BaseService): def enable_user(self, uid, password): """ Enable a user """ if self.method == "xmpp": - return False + return True else: username, server = uid.split("@") return self.ejctl.enable_user(server, username, password) diff --git a/sso/services/jabber/xmppclient.py b/sso/services/jabber/xmppclient.py index 9828d10..fc4ffb9 100644 --- a/sso/services/jabber/xmppclient.py +++ b/sso/services/jabber/xmppclient.py @@ -1,5 +1,8 @@ import time import xmpp +import random +import hashlib +import settings class JabberAdmin(): """ Adds a jabber user to a remote Jabber server """ @@ -119,6 +122,36 @@ class JabberAdmin(): return False + def disableuser(self, username): + try: + self.connect() + except: + return False + + pass = hashlib.sha1('%s%s%s' % (username, settings.SECRET_KEY, random.randint(0, 2147483647))).hexdigest() + self.resetpassword(username, pass) + self.kickuser(username) + + def kickuser(self, username): + try: + self.connect() + except: + return False + + # Send request and get the Session ID + resp = self._client.SendAndWaitForResponse(self._construct_iq_req('http://jabber.org/protocol/commands', 'http://jabber.org/protocol/admin#end-user-session')) + sessionid = resp.getTagAttr('command','sessionid') + + values = [ ('hidden', 'FORM_TYPE', 'http://jabber.org/protocol/admin'), + ('jid-single', 'accountjid', username) ] + + iq = self._construct_form('http://jabber.org/protocol/commands', 'http://jabber.org/protocol/admin#end-user-session', sessionid, values) + + # Send request and pray for the best + resp = self._client.SendAndWaitForResponse(iq) + + return True + def checkuser(self, username): try: self.connect() From f47917ac26563431fc6aaa5d30d683b4530aceff Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Wed, 24 Mar 2010 11:09:41 +0000 Subject: [PATCH 029/116] Add result checking to kickuser --- sso/services/jabber/xmppclient.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/sso/services/jabber/xmppclient.py b/sso/services/jabber/xmppclient.py index fc4ffb9..7044a94 100644 --- a/sso/services/jabber/xmppclient.py +++ b/sso/services/jabber/xmppclient.py @@ -129,8 +129,10 @@ class JabberAdmin(): return False pass = hashlib.sha1('%s%s%s' % (username, settings.SECRET_KEY, random.randint(0, 2147483647))).hexdigest() - self.resetpassword(username, pass) - self.kickuser(username) + if self.resetpassword(username, pass): + return self.kickuser(username) + else: + return False def kickuser(self, username): try: @@ -150,7 +152,10 @@ class JabberAdmin(): # Send request and pray for the best resp = self._client.SendAndWaitForResponse(iq) - return True + if resp.getAttrs()['type'] == "result": + return True + else: + return False def checkuser(self, username): try: From 87bb8e37fbcc19375bc034afbf8365547185f767 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Wed, 24 Mar 2010 11:10:30 +0000 Subject: [PATCH 030/116] Dont use settings secretkey for password resets --- sso/services/jabber/xmppclient.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sso/services/jabber/xmppclient.py b/sso/services/jabber/xmppclient.py index 7044a94..2de63dd 100644 --- a/sso/services/jabber/xmppclient.py +++ b/sso/services/jabber/xmppclient.py @@ -2,7 +2,6 @@ import time import xmpp import random import hashlib -import settings class JabberAdmin(): """ Adds a jabber user to a remote Jabber server """ @@ -128,7 +127,7 @@ class JabberAdmin(): except: return False - pass = hashlib.sha1('%s%s%s' % (username, settings.SECRET_KEY, random.randint(0, 2147483647))).hexdigest() + pass = hashlib.sha1('%s%s%s' % (username, random.randint(0, 2147483647))).hexdigest() if self.resetpassword(username, pass): return self.kickuser(username) else: From 67b9524ea9b44c789768a78e9d1ee52a6d6e6274 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Wed, 24 Mar 2010 11:12:10 +0000 Subject: [PATCH 031/116] Small cleanup, dont assign a var if its only used one --- sso/services/jabber/xmppclient.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sso/services/jabber/xmppclient.py b/sso/services/jabber/xmppclient.py index 2de63dd..2f0642c 100644 --- a/sso/services/jabber/xmppclient.py +++ b/sso/services/jabber/xmppclient.py @@ -127,8 +127,7 @@ class JabberAdmin(): except: return False - pass = hashlib.sha1('%s%s%s' % (username, random.randint(0, 2147483647))).hexdigest() - if self.resetpassword(username, pass): + if self.resetpassword(username, hashlib.sha1('%s%s%s' % (username, random.randint(0, 2147483647))).hexdigest()): return self.kickuser(username) else: return False From f15993958f1cc335b942a0495f9115034b5335d2 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Wed, 24 Mar 2010 11:37:11 +0000 Subject: [PATCH 032/116] api_status_description now provides english description of API key status --- eve_api/models/api_player.py | 6 ++++++ templates/sso/profile.html | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/eve_api/models/api_player.py b/eve_api/models/api_player.py index b39d019..4572b9d 100644 --- a/eve_api/models/api_player.py +++ b/eve_api/models/api_player.py @@ -39,6 +39,12 @@ class EVEAccount(EVEAPIModel): verbose_name="API Status", help_text="End result of the last attempt at updating this object from the API.") + @property + def api_status_description(self): + for choice in API_STATUS_CHOICES: + if choice[0] == self.api_status: + return choice[1] + def in_corp(self, corpid): for char in self.characters.all(): if char.corporation_id == corpid: diff --git a/templates/sso/profile.html b/templates/sso/profile.html index 6af6301..fcb5177 100644 --- a/templates/sso/profile.html +++ b/templates/sso/profile.html @@ -63,7 +63,7 @@ setup.

{{ acc.api_user_id }} {{ acc.api_key }} {{ acc.description }} - {{ acc.api_status }} + {{ acc.api_status_description }} Delete From 3d40d387228c8eb076bf8013526010d9f0c9457a Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Wed, 24 Mar 2010 14:24:31 +0000 Subject: [PATCH 033/116] Fixed string formatting, causing crash on cronjob run --- sso/cron.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sso/cron.py b/sso/cron.py index 8aa496d..cb2396e 100644 --- a/sso/cron.py +++ b/sso/cron.py @@ -34,7 +34,7 @@ class RemoveInvalidUsers(): pass else: if not servacc.active: - self._logger.info("User % is now in a allowed group for %s, enabling account" % (user.username, servacc.service)) + self._logger.info("User %s is now in a allowed group for %s, enabling account" % (user.username, servacc.service)) servacc.active = 1 servacc.save() servacc.user.message_set.create(message="Your %s account has been re-enabled, you may need to reset your password to access this service again." % (servacc.service)) From 337036247e0cd51d5576d1985a456a244d320fec Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Wed, 24 Mar 2010 14:32:05 +0000 Subject: [PATCH 034/116] Added warning about reactiviating inactive accounts, and set the crontab to 5 mins --- cronjobs.txt | 6 +++--- sso/views.py | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cronjobs.txt b/cronjobs.txt index 8d94013..f7fad56 100644 --- a/cronjobs.txt +++ b/cronjobs.txt @@ -1,5 +1,5 @@ ROOT=$HOME/auth/auth/ -@daily $ROOT/run-cron.py reddit.cron UpdateAPIs -@daily $ROOT/run-cron.py eve_api.cron UpdateAPIs -@hourly $ROOT/run-cron.py sso.cron RemoveInvalidUsers +*/5 * * * * $ROOT/run-cron.py reddit.cron UpdateAPIs +@daily $ROOT/run-cron.py eve_api.cron UpdateAPIs +@hourly $ROOT/run-cron.py sso.cron RemoveInvalidUsers diff --git a/sso/views.py b/sso/views.py index 474d7ff..8a27171 100644 --- a/sso/views.py +++ b/sso/views.py @@ -85,6 +85,8 @@ def eveapi_add(request): acc.description = form.cleaned_data['description'] acc.save() request.user.message_set.create(message="EVE API successfully added.") + if len(ServiceAccount.objects.filter(user=request.user, active=0)) > 0: + request.user.message_set.create(message="It can take up to 10 minutes for inactive accounts to be reenabled, please check back later.") request.user.get_profile().update_access() From 07450ca5ddc63ac540fe288183e0a31ce200be43 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Wed, 24 Mar 2010 14:52:08 +0000 Subject: [PATCH 035/116] Moved service enable/disable functions to the update_access() function on the user profile This will remove the need for the cronjob, but not totally, best still to run the cronjob to catch situations where the permission changes can be missed. --- cronjobs.txt | 4 ++-- run-cron.py | 13 +++++++------ sso/cron.py | 26 -------------------------- sso/models.py | 33 ++++++++++++++++++++++++++++++++- 4 files changed, 41 insertions(+), 35 deletions(-) diff --git a/cronjobs.txt b/cronjobs.txt index f7fad56..4bec567 100644 --- a/cronjobs.txt +++ b/cronjobs.txt @@ -1,5 +1,5 @@ ROOT=$HOME/auth/auth/ -*/5 * * * * $ROOT/run-cron.py reddit.cron UpdateAPIs +@daily $ROOT/run-cron.py reddit.cron UpdateAPIs @daily $ROOT/run-cron.py eve_api.cron UpdateAPIs -@hourly $ROOT/run-cron.py sso.cron RemoveInvalidUsers +*/10 * * * * $ROOT/run-cron.py sso.cron RemoveInvalidUsers diff --git a/run-cron.py b/run-cron.py index 4adf13f..e5830ed 100755 --- a/run-cron.py +++ b/run-cron.py @@ -2,12 +2,13 @@ """Executes a Django cronjob""" import sys +import logging from django.core.management import setup_environ import settings setup_environ(settings) -logging.basicConfig(level=logging.INFO) +logging.basicConfig(level=logging.DEBUG) log = logging.getLogger('runcron') try: @@ -19,11 +20,11 @@ for i in sys.argv[1].split(".")[1:]: mod = getattr(mod, i) cron_class = getattr(mod, sys.argv[2])() -log.info("Starting Job %s in %s" % (sys.argv[2], sys.argv[1]) +log.info("Starting Job %s in %s" % (sys.argv[2], sys.argv[1])) -try: - cron_class.job() -except: - log.error("Error executing job, aborting.") +#try: +cron_class.job() +#except: +# log.error("Error executing job, aborting.") log.info("Job complete") diff --git a/sso/cron.py b/sso/cron.py index cb2396e..72a7e8e 100644 --- a/sso/cron.py +++ b/sso/cron.py @@ -23,30 +23,4 @@ class RemoveInvalidUsers(): # For each user, update access list based on Corp details user.get_profile().update_access() - # Check each service account and disable access if they're not allowed - for servacc in ServiceAccount.objects.filter(user=user): - if not (set(user.groups.all()) & set(servacc.service.groups.all())): - if servacc.active: - self._logger.info("User %s is not in allowed group for %s, deleting account" % (user.username, servacc.service)) - servacc.active = 0 - servacc.save() - servacc.user.message_set.create(message="Your %s account has been disabled due to lack of permissions. If this is incorrect, check your API keys." % (servacc.service)) - pass - else: - if not servacc.active: - self._logger.info("User %s is now in a allowed group for %s, enabling account" % (user.username, servacc.service)) - servacc.active = 1 - servacc.save() - servacc.user.message_set.create(message="Your %s account has been re-enabled, you may need to reset your password to access this service again." % (servacc.service)) - pass - - # For users set to not active, delete all accounts - if not user.is_active: - print "User %s is inactive, disabling related service accounts" % user.username - for servacc in ServiceAccount.objects.filter(user=user): - servacc.active = 0 - servacc.save() - pass - - diff --git a/sso/models.py b/sso/models.py index 55c079a..78afc1f 100644 --- a/sso/models.py +++ b/sso/models.py @@ -1,5 +1,6 @@ import re import unicodedata +import logging from django.db import models from django.db.models import signals @@ -36,10 +37,17 @@ class SSOUser(models.Model): icq = models.CharField("ICQ", max_length=15, blank=True) xmpp = models.CharField("XMPP", max_length=200, blank=True) + @property + def _log(self): + if not hasattr(self, '__log'): + self.__log = logging.getLogger(self.__class__.__name__) + return self.__log + def update_access(self): """ Steps through each Eve API registered to the user and updates their group access accordingly """ + self._log.debug("Update - User %s" % self.user) # Create a list of all Corp groups corpgroups = [] for corp in EVEPlayerCorporation.objects.all(): @@ -64,7 +72,30 @@ class SSOUser(models.Model): for g in addgroups: self.user.groups.add(g) - print "%s, Add: %s, Del: %s, Current: %s" % (self.user, addgroups, delgroups, self.user.groups.all()) + # For users set to not active, delete all accounts + if not self.user.is_active: + self._log.debug("Inactive - User %s" % (self.user)) + for servacc in ServiceAccount.objects.filter(user=self.user): + servacc.active = 0 + servacc.save() + pass + + # For each of the user's services, check they're in a valid group for it and enable/disable as needed. + for servacc in ServiceAccount.objects.filter(user=self.user): + if not (set(self.user.groups.all()) & set(servacc.service.groups.all())): + if servacc.active: + servacc.active = 0 + servacc.save() + self._log.debug("Disabled - User %s, Acc %s" % (self.user, servacc.service)) + servacc.user.message_set.create(message="Your %s account has been disabled due to lack of permissions. If this is incorrect, check your API keys to see if they are valid" % (servacc.service)) + pass + else: + if not servacc.active: + servacc.active = 1 + servacc.save() + self._log.debug("Enabled - User %s, Acc %s" % (self.user, servacc.service)) + servacc.user.message_set.create(message="Your %s account has been re-enabled, you may need to reset your password to access this service again" % (servacc.service)) + pass def __str__(self): return self.user.__str__() From 45929882631537297daf9e14786f35ed93a6d852 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Tue, 30 Mar 2010 22:23:55 +0100 Subject: [PATCH 036/116] Warn on errors deleting a service account, also avoids deleting the record --- sso/models.py | 3 ++- sso/views.py | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/sso/models.py b/sso/models.py index 78afc1f..c6f3d88 100644 --- a/sso/models.py +++ b/sso/models.py @@ -181,6 +181,7 @@ class ServiceAccount(models.Model): def pre_delete_listener( **kwargs ): api = kwargs['instance'].service.api_class if api.check_user(kwargs['instance'].service_uid): - api.delete_user(kwargs['instance'].service_uid) + if not api.delete_user(kwargs['instance'].service_uid): + raise ServiceError('Unable to delete account on related service') signals.pre_delete.connect(ServiceAccount.pre_delete_listener, sender=ServiceAccount) diff --git a/sso/views.py b/sso/views.py index 8a27171..6487f8a 100644 --- a/sso/views.py +++ b/sso/views.py @@ -158,8 +158,12 @@ def service_del(request, serviceid=0): if request.method == 'POST': if 'confirm-delete' in request.POST: - acc.delete() - request.user.message_set.create(message="Service account successfully deleted.") + try: + acc.delete() + except ServiceError: + request.user.message_set.create(message="Error deleting the service account, try again later.") + else: + request.user.message_set.create(message="Service account successfully deleted.") else: return render_to_response('sso/serviceaccount/deleteconfirm.html', locals(), context_instance=RequestContext(request)) From d9f75ca74b1d4744ac5aa697d6582b665652fe63 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Tue, 30 Mar 2010 22:30:45 +0100 Subject: [PATCH 037/116] Updated template to show status description --- templates/sso/lookup/user.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/sso/lookup/user.html b/templates/sso/lookup/user.html index d89327f..8b43823 100644 --- a/templates/sso/lookup/user.html +++ b/templates/sso/lookup/user.html @@ -31,7 +31,7 @@ {{ acc.api_user_id }} {{ acc.api_key }} {{ acc.description }} - {{ acc.api_status }} + {{ acc.api_status_description }} {% endfor %} From 6f3dbad09ea78650fe1267a31cffebbb682fa6a9 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Tue, 30 Mar 2010 22:32:42 +0100 Subject: [PATCH 038/116] Updated cronjobs --- cronjobs.txt | 4 ++-- reddit/cron.py | 24 +++++++++++++++++++----- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/cronjobs.txt b/cronjobs.txt index 4bec567..2b01273 100644 --- a/cronjobs.txt +++ b/cronjobs.txt @@ -1,5 +1,5 @@ -ROOT=$HOME/auth/auth/ +ROOT=/home/matalok/auth/auth @daily $ROOT/run-cron.py reddit.cron UpdateAPIs @daily $ROOT/run-cron.py eve_api.cron UpdateAPIs -*/10 * * * * $ROOT/run-cron.py sso.cron RemoveInvalidUsers +*/10 * * * * $ROOT/run-cron.py sso.cron RemoveInvalidUsers > $ROOT/auth-update.log 2>&1 diff --git a/reddit/cron.py b/reddit/cron.py index 21ffab7..a9ee0dd 100644 --- a/reddit/cron.py +++ b/reddit/cron.py @@ -1,4 +1,5 @@ import time +import datetime import logging from reddit.models import RedditAccount @@ -13,12 +14,25 @@ class UpdateAPIs(): if not hasattr(self, '__logger'): self.__logger = logging.getLogger(__name__) return self.__logger + + last_update_delay = 604800 def job(self): - for acc in RedditAccount.objects.all(): - acc.api_update() - acc.save() - time.sleep(2) + delta = datetime.timedelta(seconds=self.last_update_delay) + + print delta + self._logger.debug("Updating accounts older than %s" % (datetime.datetime.now() - delta)) + + for acc in RedditAccount.objects.filter(last_update__lt=(datetime.datetime.now() - delta)): + self._logger.info("Updating %s" % acc.username) + + try: + acc.api_update() + except RedditAccount.DoesNotExist: + acc.delete() + else: + acc.save() + time.sleep(.5) class APIKeyParser: @@ -38,7 +52,7 @@ class APIKeyParser: def __str__(self): return "%s:%s" % (self.user_id, self.api_key) -class ProcessInbox(Job): +class ProcessInbox(): """ Grabs all Reddit Mail and processes any new applications """ From ead14d2ac0812f04e49c844178f13402893c0872 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Tue, 30 Mar 2010 22:33:10 +0100 Subject: [PATCH 039/116] Updated startup script --- start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/start.sh b/start.sh index 435c0a9..67ab744 100755 --- a/start.sh +++ b/start.sh @@ -1,3 +1,3 @@ #!/bin/sh -/usr/bin/python ./manage.py runfcgi method=threaded host=127.0.0.1 port=9981 daemonize=true pidfile=./auth.pid +/usr/bin/python ./manage.py runfcgi method=threaded host=127.0.0.1 port=9981 pidfile=./auth.pid From c2c87df90fe49adf748e669eebbc887ced41c938 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Wed, 31 Mar 2010 02:34:20 +0100 Subject: [PATCH 040/116] Mumble service now uses corp/alliance tag --- sso/services/mumble/__init__.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/sso/services/mumble/__init__.py b/sso/services/mumble/__init__.py index bc18ef6..7a844b2 100644 --- a/sso/services/mumble/__init__.py +++ b/sso/services/mumble/__init__.py @@ -7,13 +7,25 @@ class MumbleService(BaseService): settings = { 'require_user': True, 'require_password': True, - 'provide_login': False } + 'provide_login': False, + 'use_corptag': False } def _get_server(self): return Mumble.objects.get(id=settings.MUMBLE_SERVER_ID) def add_user(self, username, password, **kwargs): """ Add a user, returns a UID for that user """ + + if 'character' in kwargs and self.settings['use_corptag']: + if kwargs['character'].corporation: + if kwargs['character'].corporation.alliance: + tag = kwargs['character'].corporation.alliance.ticker + else: + tag = kwargs['character'].corporation.ticker + + if tag: + username = "[%s] %s" % (tag, username) + mumbleuser = MumbleUser() mumbleuser.name = username mumbleuser.password = password From 70f41a73b3928fabc8980f0dfd5c36be7c64d3b6 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Wed, 31 Mar 2010 02:38:17 +0100 Subject: [PATCH 041/116] Added service error to import, oops --- sso/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sso/views.py b/sso/views.py index 6487f8a..2cf8e17 100644 --- a/sso/views.py +++ b/sso/views.py @@ -12,7 +12,7 @@ from eve_api.api_exceptions import APIAuthException, APINoUserIDException from eve_api.api_puller.accounts import import_eve_account from eve_api.models.api_player import EVEAccount, EVEPlayerCharacter -from sso.models import ServiceAccount, Service, SSOUser, ExistingUser +from sso.models import ServiceAccount, Service, SSOUser, ExistingUser, ServiceError from sso.forms import EveAPIForm, UserServiceAccountForm, RedditAccountForm, UserLookupForm from reddit.models import RedditAccount From f0791e54170981971e868219c331c82dcf498867 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Wed, 31 Mar 2010 02:50:31 +0100 Subject: [PATCH 042/116] Enable corptag usage in Mumble --- sso/services/mumble/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sso/services/mumble/__init__.py b/sso/services/mumble/__init__.py index 7a844b2..d54e779 100644 --- a/sso/services/mumble/__init__.py +++ b/sso/services/mumble/__init__.py @@ -8,7 +8,7 @@ class MumbleService(BaseService): settings = { 'require_user': True, 'require_password': True, 'provide_login': False, - 'use_corptag': False } + 'use_corptag': True } def _get_server(self): return Mumble.objects.get(id=settings.MUMBLE_SERVER_ID) From e656a6343c7070ed9fcf343421862860d87c326b Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Wed, 31 Mar 2010 02:53:20 +0100 Subject: [PATCH 043/116] Remove square brackets due to MumbleFail --- sso/services/mumble/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sso/services/mumble/__init__.py b/sso/services/mumble/__init__.py index d54e779..2772d96 100644 --- a/sso/services/mumble/__init__.py +++ b/sso/services/mumble/__init__.py @@ -24,7 +24,7 @@ class MumbleService(BaseService): tag = kwargs['character'].corporation.ticker if tag: - username = "[%s] %s" % (tag, username) + username = "%s - %s" % (tag, username) mumbleuser = MumbleUser() mumbleuser.name = username From 1d8a3c0c34d0469080721b76307dbf0f737b3bcb Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Wed, 31 Mar 2010 02:58:22 +0100 Subject: [PATCH 044/116] War on spaces, Mumble fix again --- sso/services/mumble/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sso/services/mumble/__init__.py b/sso/services/mumble/__init__.py index 2772d96..a365a57 100644 --- a/sso/services/mumble/__init__.py +++ b/sso/services/mumble/__init__.py @@ -24,7 +24,7 @@ class MumbleService(BaseService): tag = kwargs['character'].corporation.ticker if tag: - username = "%s - %s" % (tag, username) + username = "[%s]%s" % (tag, username) mumbleuser = MumbleUser() mumbleuser.name = username From 3a6f14319bf8a6d218cf669aab1a32e51603bc2f Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Wed, 31 Mar 2010 23:15:49 +0100 Subject: [PATCH 045/116] Added password reset function --- sso/services/mumble/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sso/services/mumble/__init__.py b/sso/services/mumble/__init__.py index a365a57..f6f3faf 100644 --- a/sso/services/mumble/__init__.py +++ b/sso/services/mumble/__init__.py @@ -69,6 +69,10 @@ class MumbleService(BaseService): mumbleuser.password = password mumbleuser.save() + def reset_password(self, uid, password): + """ Reset the user's password """ + self.enable_user(uid, password) + def login(uid): """ Login the user and provide cookies back """ pass From da216c81a33d0774ece9f3fd7f63230bcd748599 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Wed, 31 Mar 2010 23:18:14 +0100 Subject: [PATCH 046/116] Fixed return codes --- sso/services/mumble/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sso/services/mumble/__init__.py b/sso/services/mumble/__init__.py index f6f3faf..a8b56c3 100644 --- a/sso/services/mumble/__init__.py +++ b/sso/services/mumble/__init__.py @@ -50,6 +50,7 @@ class MumbleService(BaseService): """ Delete a user by uid """ mumbleuser = MumbleUser.objects.get(name=uid, server=self._get_server()) mumbleuser.delete() + return True def disable_user(self, uid): """ Disable a user by uid """ @@ -59,6 +60,7 @@ class MumbleService(BaseService): return False mumbleuser.password = "" mumbleuser.save() + return True def enable_user(self, uid, password): """ Enable a user by uid """ @@ -68,10 +70,11 @@ class MumbleService(BaseService): return False mumbleuser.password = password mumbleuser.save() + return True def reset_password(self, uid, password): """ Reset the user's password """ - self.enable_user(uid, password) + return self.enable_user(uid, password) def login(uid): """ Login the user and provide cookies back """ From 4c6b5ddd9f01696023dad7536d8463901dbfdb63 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 2 Apr 2010 18:44:14 +0100 Subject: [PATCH 047/116] Change the userlookup to case insensitive. --- sso/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sso/views.py b/sso/views.py index 2cf8e17..7acbe07 100644 --- a/sso/views.py +++ b/sso/views.py @@ -269,13 +269,13 @@ def user_lookup(request): users = None uids = [] if form.cleaned_data['type'] == '1': - users = User.objects.filter(username__contains=form.cleaned_data['username']) + users = User.objects.filter(username__icontains=form.cleaned_data['username']) elif form.cleaned_data['type'] == '2': - uid = EVEAccount.objects.filter(characters__name__contains=form.cleaned_data['username']).values('user') + uid = EVEAccount.objects.filter(characters__name__icontains=form.cleaned_data['username']).values('user') for u in uid: uids.append(u['user']) users = User.objects.filter(id__in=uids) elif form.cleaned_data['type'] == '3': - uid = RedditAccount.objects.filter(username__contains=form.cleaned_data['username']).values('user') + uid = RedditAccount.objects.filter(username__icontains=form.cleaned_data['username']).values('user') for u in uid: uids.append(u['user']) users = User.objects.filter(id__in=uids) else: From 33914c7a8dfa0db64a35028885f0983c8d13de6f Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 2 Apr 2010 19:02:52 +0100 Subject: [PATCH 048/116] Added evolutions from live to SSO --- sso/evolutions/__init__.py | 1 + sso/evolutions/uid_fix.py | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 sso/evolutions/__init__.py create mode 100644 sso/evolutions/uid_fix.py diff --git a/sso/evolutions/__init__.py b/sso/evolutions/__init__.py new file mode 100644 index 0000000..5f070df --- /dev/null +++ b/sso/evolutions/__init__.py @@ -0,0 +1 @@ +SEQUENCE = ['uid_fix'] diff --git a/sso/evolutions/uid_fix.py b/sso/evolutions/uid_fix.py new file mode 100644 index 0000000..913f707 --- /dev/null +++ b/sso/evolutions/uid_fix.py @@ -0,0 +1,6 @@ +from django_evolution.mutations import * +from django.db import models + +MUTATIONS = [ + AddField('ServiceAccount', 'service_uid', models.CharField, max_length=200, blank=False) +] From 795066dfaabbf2a668c131ed40ae1845c1659543 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 2 Apr 2010 19:04:01 +0100 Subject: [PATCH 049/116] Added validated field to Reddit account --- reddit/evolutions/__init__.py | 1 + reddit/evolutions/validation-field.py | 7 +++++++ reddit/models.py | 6 +++--- 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 reddit/evolutions/__init__.py create mode 100644 reddit/evolutions/validation-field.py diff --git a/reddit/evolutions/__init__.py b/reddit/evolutions/__init__.py new file mode 100644 index 0000000..f330e9e --- /dev/null +++ b/reddit/evolutions/__init__.py @@ -0,0 +1 @@ +SEQUENCE = ['validation-field'] diff --git a/reddit/evolutions/validation-field.py b/reddit/evolutions/validation-field.py new file mode 100644 index 0000000..78ce982 --- /dev/null +++ b/reddit/evolutions/validation-field.py @@ -0,0 +1,7 @@ +from django_evolution.mutations import * +from django.db import models + +MUTATIONS = [ + AddField('RedditAccount', 'validated', models.BooleanField), +] + diff --git a/reddit/models.py b/reddit/models.py index 5fc8b77..ee3e9aa 100644 --- a/reddit/models.py +++ b/reddit/models.py @@ -14,11 +14,11 @@ class RedditAccount(models.Model): username = models.CharField("Reddit Username", max_length=32, blank=False) reddit_id = models.CharField("Reddit ID", max_length=32) - date_created = models.DateTimeField("Date Created") - link_karma = models.IntegerField("Link Karma") comment_karma = models.IntegerField("Comment Karma") + validated = models.BooleanField("Validated") + date_created = models.DateTimeField("Date Created") last_update = models.DateTimeField("Last Update from API") def api_update(self): @@ -32,8 +32,8 @@ class RedditAccount(models.Model): self.link_karma = data['link_karma'] self.comment_karma = data['comment_karma'] self.reddit_id = data['id'] - self.date_created = datetime.fromtimestamp(data['created_utc']) + self.date_created = datetime.fromtimestamp(data['created_utc']) self.last_update = datetime.now() class Meta: From 83a91131275f2d5353f52afeba114caaf861f0f2 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 2 Apr 2010 19:32:57 +0100 Subject: [PATCH 050/116] Added validation processes for reddit accounts --- cronjobs.txt | 3 ++- reddit/cron.py | 17 +++++++++-------- settings.py | 8 ++++++++ 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/cronjobs.txt b/cronjobs.txt index 2b01273..8333f68 100644 --- a/cronjobs.txt +++ b/cronjobs.txt @@ -1,5 +1,6 @@ ROOT=/home/matalok/auth/auth -@daily $ROOT/run-cron.py reddit.cron UpdateAPIs +@daily $ROOT/run-cron.py reddit.cron UpdateAPIs +*/10 * * * * $ROOT/run-cron.py reddit.cron ProcessValidations @daily $ROOT/run-cron.py eve_api.cron UpdateAPIs */10 * * * * $ROOT/run-cron.py sso.cron RemoveInvalidUsers > $ROOT/auth-update.log 2>&1 diff --git a/reddit/cron.py b/reddit/cron.py index a9ee0dd..9ef11a8 100644 --- a/reddit/cron.py +++ b/reddit/cron.py @@ -52,20 +52,21 @@ class APIKeyParser: def __str__(self): return "%s:%s" % (self.user_id, self.api_key) -class ProcessInbox(): +class ProcessValidations(): """ - Grabs all Reddit Mail and processes any new applications + Grabs all Reddit Mail and processes validations """ def job(self): - inbox = Inbox(settings.REDDIT_USER, settings.REDDIT_PASSWORD) + inbox = Inbox(settings.REDDIT_USER, settings.REDDIT_PASSWD) for msg in inbox: if not msg.was_comment and msg.new: try: - key = APIKeyParser(msg.body) - except: - pass - else: - print key.username + acc = RedditAccount.objects.get(username_iexact=msg.username) + if not acc.validated and msg.body == acc.user.username: + acc.validated = True + acc.save() + except RedditAccount.DoesNotExist: + continue diff --git a/settings.py b/settings.py index 5d0778d..ee86694 100644 --- a/settings.py +++ b/settings.py @@ -93,6 +93,14 @@ FORCE_SCRIPT_NAME="" DEFAULT_FROM_EMAIL = "bot@auth.dredd.it" ACCOUNT_ACTIVATION_DAYS = 14 +### Reddit Settings + +# Username to validate accounts from +REDDIT_USER = 'DredditVerification' + +# Password for validatio account +REDDIT_PASSWD = '' + ### Jabber Service Settings # Vhost to add users to From abd1644098138a001059970f9bdd5c2ac590dbc9 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 2 Apr 2010 19:35:53 +0100 Subject: [PATCH 051/116] Added default rank and mark emails as valid --- sso/services/miningbuddy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sso/services/miningbuddy/__init__.py b/sso/services/miningbuddy/__init__.py index e8647a6..5a654d9 100644 --- a/sso/services/miningbuddy/__init__.py +++ b/sso/services/miningbuddy/__init__.py @@ -16,7 +16,7 @@ class MiningBuddyService(BaseService): 'provide_login': False } - SQL_ADD_USER = r"INSERT INTO users (username, password, email, emailvalid, confirmed) VALUES (%s, %s, %s, 0, 1)" + SQL_ADD_USER = r"INSERT INTO users (username, password, email, emailvalid, confirmed, rank) VALUES (%s, %s, %s, 1, 1, 2)" SQL_ADD_API = r"INSERT INTO api_keys (userid, time, apiID, apiKey, api_valid, charid) values (%s, %s, %s, %s, 1, %s)" SQL_DIS_USER = r"UPDATE users SET canLogin = 0 WHERE username = %s" SQL_ENABLE_USER = r"UPDATE users SET canLogin = 1, password = %s WHERE username = %s" From be12cbf90afd18ee58bc1d91fe462f7b08c83760 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 2 Apr 2010 19:40:01 +0100 Subject: [PATCH 052/116] Added initial value to evolution --- reddit/evolutions/validation-field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reddit/evolutions/validation-field.py b/reddit/evolutions/validation-field.py index 78ce982..53950d3 100644 --- a/reddit/evolutions/validation-field.py +++ b/reddit/evolutions/validation-field.py @@ -2,6 +2,6 @@ from django_evolution.mutations import * from django.db import models MUTATIONS = [ - AddField('RedditAccount', 'validated', models.BooleanField), + AddField('RedditAccount', 'validated', models.BooleanField, initial=False), ] From ed05f50217616c339364004e151f08e9b761c5c9 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 2 Apr 2010 19:50:46 +0100 Subject: [PATCH 053/116] Fixed validation check --- reddit/cron.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reddit/cron.py b/reddit/cron.py index 9ef11a8..f5cc2fb 100644 --- a/reddit/cron.py +++ b/reddit/cron.py @@ -63,7 +63,7 @@ class ProcessValidations(): for msg in inbox: if not msg.was_comment and msg.new: try: - acc = RedditAccount.objects.get(username_iexact=msg.username) + acc = RedditAccount.objects.get(username__iexact=msg.author) if not acc.validated and msg.body == acc.user.username: acc.validated = True acc.save() From ef65dfe145915ff7b947600d24e0326a28151f56 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 2 Apr 2010 19:55:25 +0100 Subject: [PATCH 054/116] Added verification to the profile page --- templates/sso/profile.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/templates/sso/profile.html b/templates/sso/profile.html index fcb5177..0f39cbb 100644 --- a/templates/sso/profile.html +++ b/templates/sso/profile.html @@ -79,14 +79,17 @@ setup.

Reddit Accounts

This is a list of all your current linked Reddit accounts

{% if redditaccounts %} +

To verify your Reddit account, login on Reddit then click +this link and put your Auth username in the message.

- + {% for acc in redditaccounts %} + {% endfor %} From ffe07995020c39855bd3162bb6f935b6b211732e Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 2 Apr 2010 20:03:08 +0100 Subject: [PATCH 055/116] Ignore errors on updating corporation details, avoids the DS1 issue for the moment --- eve_api/cron.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/eve_api/cron.py b/eve_api/cron.py index a1d3826..fa856c3 100644 --- a/eve_api/cron.py +++ b/eve_api/cron.py @@ -31,4 +31,8 @@ class UpdateAPIs(): acc.save() for corp in EVEPlayerCorporation.objects.all(): - corp.query_and_update_corp() + try: + corp.query_and_update_corp() + except: + self._logger.error('Error updating %s' % corp) + continue From 342ccfc887bb729aaf9b1a3e6130cf23203deb75 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 2 Apr 2010 20:04:07 +0100 Subject: [PATCH 056/116] Import settings into reddit cron --- reddit/cron.py | 1 + 1 file changed, 1 insertion(+) diff --git a/reddit/cron.py b/reddit/cron.py index f5cc2fb..bb191b0 100644 --- a/reddit/cron.py +++ b/reddit/cron.py @@ -1,6 +1,7 @@ import time import datetime import logging +import settings from reddit.models import RedditAccount from reddit.api import Inbox From 28d3ee66d8997d92cc0e80cf1e4211f19c5f15df Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 2 Apr 2010 20:52:37 +0100 Subject: [PATCH 057/116] Now links to user's character profiles --- sso/views.py | 2 +- templates/sso/lookup/user.html | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/sso/views.py b/sso/views.py index 7acbe07..83cc644 100644 --- a/sso/views.py +++ b/sso/views.py @@ -254,7 +254,7 @@ def user_view(request, username=None): for acc in eveaccounts: chars = acc.characters.all() for char in chars: - characters.append({'name': char.name, 'corp': char.corporation.name}) + characters.append({'id': char.id, 'name': char.name, 'corp': char.corporation.name}) except EVEAccount.DoesNotExist: eveaccounts = None diff --git a/templates/sso/lookup/user.html b/templates/sso/lookup/user.html index 8b43823..1279ce3 100644 --- a/templates/sso/lookup/user.html +++ b/templates/sso/lookup/user.html @@ -41,7 +41,7 @@
UsernameCreated Date
UsernameCreated DateValidated
{{ acc.username }} {{ acc.date_created }}{% if acc.validated %}Yes{% else %}No{% endif %}
{% for char in characters %} - + {% endfor %} @@ -52,12 +52,17 @@

Reddit Accounts

{% if reddits %}
Character NameCorp
{{ char.name }}
{{ char.name }} {{ char.corp }}
- + + + + {% for acc in reddits %} + {% endfor %} +
UsernameCreated Date
UsernameCreated DateValidated
{{ acc.username }} {{ acc.date_created }}{% if acc.validated %}Yes{% else %}No{% endif %}
{% endif %} {% endif %} From da400983f340d47d6ee616f2081747bbc6232b54 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 2 Apr 2010 20:59:40 +0100 Subject: [PATCH 058/116] Eve_api module now pulls Corp if we don't have a record of it, and aborts on a error --- eve_api/api_puller/accounts.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/eve_api/api_puller/accounts.py b/eve_api/api_puller/accounts.py index 8f84d12..274bbb7 100755 --- a/eve_api/api_puller/accounts.py +++ b/eve_api/api_puller/accounts.py @@ -59,6 +59,11 @@ def import_eve_account(api_key, user_id): # Get this first, as it's safe. corporation_id = node.getAttribute('corporationID') corp, created = EVEPlayerCorporation.objects.get_or_create(id=corporation_id) + if not corp.name: + try: + corp.query_and_update_corp() + except: + pass # Do this last, since the things we retrieved above are used # on the EVEPlayerCharacter object's fields. character_id = node.getAttribute('characterID') From 75c1dedb6eddfcb540ee29de5ae31b99d9927d07 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 2 Apr 2010 21:48:07 +0100 Subject: [PATCH 059/116] Add validation details to the Admin interface --- reddit/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reddit/admin.py b/reddit/admin.py index 96d499d..b519628 100644 --- a/reddit/admin.py +++ b/reddit/admin.py @@ -5,8 +5,8 @@ from reddit.forms import RedditAccountForm from datetime import date class RedditAccountAdmin(admin.ModelAdmin): - list_display = ('username', 'user', 'date_created', 'link_karma', 'comment_karma', 'last_update', 'is_valid') - search_fields = ['username', 'user'] + list_display = ('username', 'user', 'date_created', 'link_karma', 'comment_karma', 'last_update', 'validated', 'is_valid') + search_fields = ['username'] fields = ('user', 'username') From 9c6272d4f5547483b5e5997b6e6ac54e2ce7e3d8 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 2 Apr 2010 21:48:30 +0100 Subject: [PATCH 060/116] Added method to update account from API in the Admin interface --- eve_api/admin.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/eve_api/admin.py b/eve_api/admin.py index 33be191..17363cc 100644 --- a/eve_api/admin.py +++ b/eve_api/admin.py @@ -4,9 +4,25 @@ Admin interface models. Automatically detected by admin.autodiscover(). from django.contrib import admin from eve_api.models import * +from eve_api.api_puller.accounts import import_eve_account + +def account_api_update(modeladmin, request, queryset): + for obj in queryset: + try: + import_eve_account(obj.api_key, obj.api_user_id) + obj.api_status = 1 + except APIAuthException: + obj.api_status = 2 + obj.save() + +account_api_update.short_description = "Update account from the EVE API" + class EVEAccountAdmin(admin.ModelAdmin): list_display = ('id', 'user', 'api_status', 'api_last_updated') search_fields = ['id'] + + actions = [account_api_update] + admin.site.register(EVEAccount, EVEAccountAdmin) class EVEPlayerCharacterAdmin(admin.ModelAdmin): From 16373185227448d06df57b2394a8af0be537bb6e Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 2 Apr 2010 23:02:45 +0100 Subject: [PATCH 061/116] Now pulls in more information from the character API. --- eve_api/api_puller/accounts.py | 85 ++++++++++++++++++++++++++-------- eve_api/app_defines.py | 37 ++++++++++++++- eve_api/models/api_player.py | 8 ++-- 3 files changed, 104 insertions(+), 26 deletions(-) diff --git a/eve_api/api_puller/accounts.py b/eve_api/api_puller/accounts.py index 274bbb7..4be73b1 100755 --- a/eve_api/api_puller/accounts.py +++ b/eve_api/api_puller/accounts.py @@ -56,31 +56,76 @@ def import_eve_account(api_key, user_id): for node in characters_node_children: try: - # Get this first, as it's safe. - corporation_id = node.getAttribute('corporationID') - corp, created = EVEPlayerCorporation.objects.get_or_create(id=corporation_id) - if not corp.name: - try: - corp.query_and_update_corp() - except: - pass - # Do this last, since the things we retrieved above are used - # on the EVEPlayerCharacter object's fields. - character_id = node.getAttribute('characterID') - pchar, created = EVEPlayerCharacter.objects.get_or_create(id=character_id) - name = node.getAttribute('name') - # Save these for last to keep the save count low. - pchar.name = name - pchar.corporation = corp - pchar.api_last_updated = datetime.utcnow() - pchar.save() - account.characters.add(pchar) + char = import_eve_character(api_key, user_id, node.getAttribute('characterID')) + if char: + account.characters.add(char) except AttributeError: # This must be a Text node, ignore it. continue - return account +def import_eve_character(api_key, user_id, character_id): + + auth_params = {'userID': user_id, 'apiKey': api_key, 'characterID': character_id } + char_doc = CachedDocument.objects.api_query('/char/CharacterSheet.xml.aspx', + params=auth_params, + no_cache=False) + + dom = minidom.parseString(char_doc.body) + if dom.getElementsByTagName('error'): + return + + nodes = dom.getElementsByTagName('result')[0].childNodes + pchar, created = EVEPlayerCharacter.objects.get_or_create(id=character_id) + + values = {} + for node in nodes: + if node.nodeType == 1: + node.normalize() + if len(node.childNodes) == 1: + values[node.tagName] = node.childNodes[0].nodeValue + else: + if node.tagName == "rowset": + continue + nv = {} + for nd in node.childNodes: + if nd.nodeType == 1: + nv[nd.tagName] = nd.childNodes[0].nodeValue + values[node.tagName] = nv + + # Get this first, as it's safe. + corporation_id = values['corporationID'] + corp, created = EVEPlayerCorporation.objects.get_or_create(id=corporation_id) + if not corp.name: + try: + corp.query_and_update_corp() + except: + pass + + name = values['name'] + # Save these for last to keep the save count low. + pchar.name = name + pchar.corporation = corp + + pchar.balance = values['balance'] + pchar.attrib_intelligence = values['attributes']['intelligence'] + pchar.attrib_charisma = values['attributes']['charisma'] + pchar.attrib_perception = values['attributes']['perception'] + pchar.attrib_willpower = values['attributes']['willpower'] + pchar.attrib_memory = values['attributes']['memory'] + + if values['gender'] == 'Male': + pchar.gender = 1 + else: + pchar.gender = 2 + + pchar.race = API_RACES[values['race']] + + pchar.api_last_updated = datetime.utcnow() + pchar.save() + + return pchar + if __name__ == "__main__": """ Test import. diff --git a/eve_api/app_defines.py b/eve_api/app_defines.py index 5da4556..75cbf66 100644 --- a/eve_api/app_defines.py +++ b/eve_api/app_defines.py @@ -12,4 +12,39 @@ API_STATUS_CHOICES = ( (API_STATUS_OK, 'OK'), (API_STATUS_AUTH_ERROR, 'Auth Error'), (API_STATUS_OTHER_ERROR, 'Other Error'), -) \ No newline at end of file +) + +API_GENDER_CHOICES = ( + (1, 'Male'), + (2, 'Female'), +) + +API_RACES_CHOICES = ( + (1, 'Sebiestor'), + (2, 'Vherokior'), + (3, 'Brutor'), + (4, 'Intaki'), + (5, 'Gallente'), + (6, 'Jin-Mei'), + (7, 'Civire'), + (8, 'Deteis'), + (9, 'Achura'), + (10, 'Amarr'), + (11, 'Khanid'), + (12, 'Ni-Kunni'), +) + +API_RACES = { + 'Sebiestor': 1, + 'Vherokior': 2, + 'Brutor': 3, + 'Intaki': 4, + 'Gallente': 5, + 'Jin-Mei': 6, + 'Civire': 7, + 'Deteis': 8, + 'Achura': 9, + 'Amarr': 10, + 'Khanid': 11, + 'Ni-Kunni': 12, +} diff --git a/eve_api/models/api_player.py b/eve_api/models/api_player.py index 4572b9d..121d1cb 100644 --- a/eve_api/models/api_player.py +++ b/eve_api/models/api_player.py @@ -7,7 +7,7 @@ from django.db import models from django.contrib.auth.models import User, Group from eve_proxy.models import CachedDocument from eve_api.managers import EVEPlayerCorporationManager, EVEPlayerAllianceManager, EVEPlayerCharacterManager -from eve_api.app_defines import API_STATUS_CHOICES, API_STATUS_PENDING +from eve_api.app_defines import API_STATUS_CHOICES, API_STATUS_PENDING, API_RACES_CHOICES, API_GENDER_CHOICES class EVEAPIModel(models.Model): """ @@ -64,10 +64,8 @@ class EVEPlayerCharacter(EVEAPIModel): """ name = models.CharField(max_length=255, blank=True, null=False) corporation = models.ForeignKey('EVEPlayerCorporation', blank=True, null=True) - # TODO: Choices field - race = models.IntegerField(blank=True, null=True) - # TODO: Choices field - gender = models.IntegerField(blank=True, null=True) + race = models.IntegerField(blank=True, null=True, choices=API_RACES_CHOICES) + gender = models.IntegerField(blank=True, null=True, choices=API_GENDER_CHOICES) balance = models.FloatField("Account Balance", blank=True, null=True) attrib_intelligence = models.IntegerField("Intelligence", blank=True, null=True) From 220e8d4d624caaa516b5b6628d29f2efa0988414 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 2 Apr 2010 23:38:58 +0100 Subject: [PATCH 062/116] Now shows actual race/gender values in character profile --- eve_api/models/api_player.py | 14 ++++++++++++++ templates/sso/character.html | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/eve_api/models/api_player.py b/eve_api/models/api_player.py index 121d1cb..eb4f20f 100644 --- a/eve_api/models/api_player.py +++ b/eve_api/models/api_player.py @@ -76,6 +76,20 @@ class EVEPlayerCharacter(EVEAPIModel): objects = EVEPlayerCharacterManager() + @property + def gender_description(): + for va in API_GENDER_CHOICES: + k, v = va + if v == self.gender: + return k + + @property + def race_description(): + for va in API_RACES_CHOICES: + k, v = va + if v == self.race: + return k + def __unicode__(self): if self.name: return "%s (%d)" % (self.name, self.id) diff --git a/templates/sso/character.html b/templates/sso/character.html index ea9ff24..0a3949e 100644 --- a/templates/sso/character.html +++ b/templates/sso/character.html @@ -7,8 +7,8 @@
    -
  • Race: {{ character.race }}
  • -
  • Gender: {{ character.gender }}
  • +
  • Race: {{ character.race_description }}
  • +
  • Gender: {{ character.gender_description }}
  • Corporation: {{ character.corporation.name }}
From d7aa896151fca2c5ae5f5b1754a64604e9598d19 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 2 Apr 2010 23:51:20 +0100 Subject: [PATCH 063/116] Added Caldari to the races --- eve_api/app_defines.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/eve_api/app_defines.py b/eve_api/app_defines.py index 75cbf66..b6d496b 100644 --- a/eve_api/app_defines.py +++ b/eve_api/app_defines.py @@ -32,6 +32,7 @@ API_RACES_CHOICES = ( (10, 'Amarr'), (11, 'Khanid'), (12, 'Ni-Kunni'), + (13, 'Caldari'), ) API_RACES = { @@ -47,4 +48,5 @@ API_RACES = { 'Amarr': 10, 'Khanid': 11, 'Ni-Kunni': 12, + 'Caldari': 13, } From 37e9ade3092ce59a6b0f740903145b128e55a9d7 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 2 Apr 2010 23:54:16 +0100 Subject: [PATCH 064/116] Oops, confused races/bloodlines --- eve_api/app_defines.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/eve_api/app_defines.py b/eve_api/app_defines.py index b6d496b..2356d04 100644 --- a/eve_api/app_defines.py +++ b/eve_api/app_defines.py @@ -20,6 +20,13 @@ API_GENDER_CHOICES = ( ) API_RACES_CHOICES = ( + (1, 'Caldari'), + (2, 'Minmatar'), + (3, 'Gallente'), + (4, 'Amarr'), +) + +API_BLOODLINES_CHOICES = ( (1, 'Sebiestor'), (2, 'Vherokior'), (3, 'Brutor'), @@ -36,6 +43,13 @@ API_RACES_CHOICES = ( ) API_RACES = { + 'Caldari': 1, + 'Minmatar': 2, + 'Gallente': 3, + 'Amarr': 4, +} + +API_BLOODLINES = { 'Sebiestor': 1, 'Vherokior': 2, 'Brutor': 3, @@ -48,5 +62,4 @@ API_RACES = { 'Amarr': 10, 'Khanid': 11, 'Ni-Kunni': 12, - 'Caldari': 13, } From 634aa2765f091e407d7b4a1e74d8f3492692f22f Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Sat, 3 Apr 2010 02:07:13 +0100 Subject: [PATCH 065/116] Fixing unicode issues in eve_api and eve_proxy. Shouldn't blow its guts on the russian alliances --- eve_api/managers.py | 5 ++--- eve_proxy/models.py | 12 +++++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/eve_api/managers.py b/eve_api/managers.py index 8633a76..733f4b7 100644 --- a/eve_api/managers.py +++ b/eve_api/managers.py @@ -79,10 +79,9 @@ class EVEPlayerCorporationManager(models.Manager): """ corp_doc = CachedDocument.objects.api_query('/corp/CorporationSheet.xml.aspx', params={'corporationID': id}) - corp_dat = corp_doc.body.decode("utf-8", "replace") # Convert incoming data to UTF-8. - dom = minidom.parseString(corp_dat) + dom = minidom.parseString(corp_doc.body.encode('utf-8')) error_node = dom.getElementsByTagName('error') @@ -93,4 +92,4 @@ class EVEPlayerCorporationManager(models.Manager): raise InvalidCorpID(id) return dom - \ No newline at end of file + diff --git a/eve_proxy/models.py b/eve_proxy/models.py index 2b58089..3d755f7 100755 --- a/eve_proxy/models.py +++ b/eve_proxy/models.py @@ -27,14 +27,12 @@ class CachedDocumentManager(models.Manager): # Retrieve the response from the server. response = conn.getresponse() # Save the response (an XML document) to the CachedDocument. - cached_doc.body = response.read() - + cached_doc.body = unicode(response.read(), 'utf-8') + try: # Parse the response via minidom - dom = minidom.parseString(cached_doc.body) + dom = minidom.parseString(cached_doc.body.encode('utf-8')) except xml.parsers.expat.ExpatError: - print "XML Parser Error:" - print cached_doc.body return # Set the CachedDocument's time_retrieved and cached_until times based @@ -97,7 +95,7 @@ class CachedDocumentManager(models.Manager): else: # Parse the document here since it was retrieved from the # database cache instead of queried for. - dom = minidom.parseString(cached_doc.body) + dom = minidom.parseString(cached_doc.body.encode('utf-8')) # Check for the presence errors. Only check the bare minimum, # generic stuff that applies to most or all queries. User-level code @@ -123,4 +121,4 @@ class CachedDocument(models.Model): cached_until = models.DateTimeField(blank=True, null=True) # The custom manager handles the querying. - objects = CachedDocumentManager() \ No newline at end of file + objects = CachedDocumentManager() From 3854aca02d15a3f8cc6f6844bd81d1ea3c9a743f Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Sat, 3 Apr 2010 03:01:36 +0100 Subject: [PATCH 066/116] Corp updates are included in the characters, so seperate corp update is optional --- eve_api/cron.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/eve_api/cron.py b/eve_api/cron.py index fa856c3..26f70dc 100644 --- a/eve_api/cron.py +++ b/eve_api/cron.py @@ -9,6 +9,8 @@ class UpdateAPIs(): Updates all Eve API elements in the database """ + settings = { 'update_corp': False } + @property def _logger(self): if not hasattr(self, '__logger'): @@ -30,9 +32,10 @@ class UpdateAPIs(): acc.save() - for corp in EVEPlayerCorporation.objects.all(): - try: - corp.query_and_update_corp() - except: - self._logger.error('Error updating %s' % corp) - continue + if self.settings['update_corp']: + for corp in EVEPlayerCorporation.objects.all(): + try: + corp.query_and_update_corp() + except: + self._logger.error('Error updating %s' % corp) + continue From 004fe1d6c8f0b74d0e36353f5d1428a39683ca28 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Sat, 3 Apr 2010 20:55:56 +0100 Subject: [PATCH 067/116] Optional setting to disable the auto password generation for Service Accounts --- settings.py | 3 +++ sso/forms.py | 7 +++++++ sso/views.py | 5 ++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/settings.py b/settings.py index ee86694..3ef8565 100644 --- a/settings.py +++ b/settings.py @@ -85,6 +85,9 @@ INSTALLED_APPS = ( # Disable the service API, used for data imports DISABLE_SERVICES = False +# Services API generates a new password for the user +GENERATE_SERVICE_PASSWORD = False + AUTH_PROFILE_MODULE = 'sso.SSOUser' LOGIN_REDIRECT_URL = "/profile" LOGIN_URL = "/login" diff --git a/sso/forms.py b/sso/forms.py index eaac5a4..5130361 100644 --- a/sso/forms.py +++ b/sso/forms.py @@ -3,6 +3,7 @@ import re from django import forms from django.contrib.auth.models import User +import settings from eve_api.models.api_player import EVEAccount, EVEPlayerCharacter from sso.models import ServiceAccount, Service from reddit.models import RedditAccount @@ -36,6 +37,12 @@ def UserServiceAccountForm(user): character = forms.ModelChoiceField(queryset=chars, required=True, empty_label=None) service = forms.ModelChoiceField(queryset=services, required=True, empty_label=None) + def __init__(self, *args, **kwargs): + super(ServiceAccountForm, self).__init__(*args, **kwargs) + if not settings.GENERATE_SERVICE_PASSWORD: + self.password = forms.CharField(widget=forms.PasswordInput, label="Password" ) + self.fields['password'] = self.password + def clean(self): if not self.cleaned_data['character'].corporation.group in self.cleaned_data['service'].groups.all(): raise forms.ValidationError("%s is not in a corporation allowed to access %s" % (self.cleaned_data['character'].name, self.cleaned_data['service'])) diff --git a/sso/views.py b/sso/views.py index 83cc644..96956ca 100644 --- a/sso/views.py +++ b/sso/views.py @@ -124,7 +124,10 @@ def service_add(request): acc.user = request.user acc.service = form.cleaned_data['service'] acc.character = form.cleaned_data['character'] - acc.password = hashlib.sha1('%s%s%s' % (form.cleaned_data['character'].name, settings.SECRET_KEY, random.randint(0, 2147483647))).hexdigest() + if settings.GENERATE_SERVICE_PASSWORD: + acc.password = hashlib.sha1('%s%s%s' % (form.cleaned_data['character'].name, settings.SECRET_KEY, random.randint(0, 2147483647))).hexdigest() + else: + acc.password = form.cleaned_data['password'] try: acc.save() From 45e303fb4ec61ed015b65f7cb4e4d95799adbbbc Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Sat, 3 Apr 2010 20:57:50 +0100 Subject: [PATCH 068/116] BaseService is always successful. --- sso/services/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sso/services/__init__.py b/sso/services/__init__.py index b7a3a76..e1d858f 100644 --- a/sso/services/__init__.py +++ b/sso/services/__init__.py @@ -36,23 +36,23 @@ class BaseService(): def check_user(self, username): """ Check if the username exists """ - pass + return False def delete_user(self, uid): """ Delete a user by uid """ - pass + return True def disable_user(self, uid): """ Disable a user by uid """ - pass + return True def enable_user(self, uid, password): """ Enable a user by uid """ - pass + return True def reset_password(self, uid, password): """ Reset the user's password """ - pass + return True def login(uid): """ Login the user and provide cookies back """ From 96de3cc5eb4bef0287479c520fe7397888d40ada Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Sat, 3 Apr 2010 21:19:39 +0100 Subject: [PATCH 069/116] Password Reset now allows for providing passwords when GENERATE_SERVICE_PASSWORD is false --- sso/forms.py | 7 +++++++ sso/views.py | 24 ++++++++++++++++-------- templates/sso/serviceaccount/reset.html | 7 ++++++- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/sso/forms.py b/sso/forms.py index 5130361..cc1eeb5 100644 --- a/sso/forms.py +++ b/sso/forms.py @@ -51,6 +51,13 @@ def UserServiceAccountForm(user): return ServiceAccountForm +class ServiceAccountResetForm(forms.Form): + def __init__(self, *args, **kwargs): + super(ServiceAccountResetForm, self).__init__(*args, **kwargs) + if not settings.GENERATE_SERVICE_PASSWORD: + self.password = forms.CharField(widget=forms.PasswordInput, label="Password" ) + self.fields['password'] = self.password + class RedditAccountForm(forms.Form): """ Reddit Account Form """ diff --git a/sso/views.py b/sso/views.py index 96956ca..247d08a 100644 --- a/sso/views.py +++ b/sso/views.py @@ -13,7 +13,7 @@ from eve_api.api_puller.accounts import import_eve_account from eve_api.models.api_player import EVEAccount, EVEPlayerCharacter from sso.models import ServiceAccount, Service, SSOUser, ExistingUser, ServiceError -from sso.forms import EveAPIForm, UserServiceAccountForm, RedditAccountForm, UserLookupForm +from sso.forms import EveAPIForm, UserServiceAccountForm, ServiceAccountResetForm, RedditAccountForm, UserLookupForm from reddit.models import RedditAccount @@ -173,7 +173,7 @@ def service_del(request, serviceid=0): return HttpResponseRedirect(reverse('sso.views.profile')) @login_required -def service_reset(request, serviceid=0, accept=0): +def service_reset(request, serviceid=0): if serviceid > 0 : try: acc = ServiceAccount.objects.get(id=serviceid) @@ -184,15 +184,23 @@ def service_reset(request, serviceid=0, accept=0): return HttpResponseRedirect(reverse('sso.views.profile')) if acc.user == request.user: - if not accept: + if not request.method == 'POST': + form = ServiceAccountResetForm() return render_to_response('sso/serviceaccount/reset.html', locals(), context_instance=RequestContext(request)) - passwd = hashlib.sha1('%s%s%s' % (acc.service_uid, settings.SECRET_KEY, random.randint(0, 2147483647))).hexdigest() + form = ServiceAccountResetForm(request.POST) + if form.is_valid(): + if settings.GENERATE_SERVICE_PASSWORD: + passwd = hashlib.sha1('%s%s%s' % (acc.service_uid, settings.SECRET_KEY, random.randint(0, 2147483647))).hexdigest() + else: + passwd = form.cleaned_data['password'] - api = acc.service.api_class - if not api.reset_password(acc.service_uid, passwd): - error = True - return render_to_response('sso/serviceaccount/resetcomplete.html', locals(), context_instance=RequestContext(request)) + api = acc.service.api_class + if not api.reset_password(acc.service_uid, passwd): + error = True + return render_to_response('sso/serviceaccount/resetcomplete.html', locals(), context_instance=RequestContext(request)) + else: + return render_to_response('sso/serviceaccount/reset.html', locals(), context_instance=RequestContext(request)) return HttpResponseRedirect(reverse('sso.views.profile')) diff --git a/templates/sso/serviceaccount/reset.html b/templates/sso/serviceaccount/reset.html index 9aae69d..03f336c 100644 --- a/templates/sso/serviceaccount/reset.html +++ b/templates/sso/serviceaccount/reset.html @@ -7,6 +7,11 @@

This service will reset your password for account {{ acc.service_uid }} on {{ acc.service }}. If you wish to continue please click the link below.

-

Reset my Password

+
+ +{{ form.as_table }} +
+ +
{% endblock %} From ca7aaf9c05d4d4d3166aecbd55b1e3e4c4882cdb Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Sun, 4 Apr 2010 00:30:30 +0100 Subject: [PATCH 070/116] Now passes email to mediawiki db, also lets options stay as none. --- sso/services/wiki/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/sso/services/wiki/__init__.py b/sso/services/wiki/__init__.py index e65ce7a..8f58cb2 100644 --- a/sso/services/wiki/__init__.py +++ b/sso/services/wiki/__init__.py @@ -14,8 +14,7 @@ class MediawikiService(BaseService): 'require_password': False, 'provide_login': False } - - SQL_ADD_USER = r"INSERT INTO user (user_name, user_password, user_newpassword, user_options, user_email) VALUES (%s, %s, '', '', '')" + SQL_ADD_USER = r"INSERT INTO user (user_name, user_password, user_newpassword, user_email) VALUES (%s, %s, '', %s)" SQL_DIS_USER = r"UPDATE user SET user_password = '', user_email = '' WHERE user_name = %s" SQL_ENABLE_USER = r"UPDATE user SET user_password = %s WHERE user_name = %s" SQL_CHECK_USER = r"SELECT user_name from user WHERE user_name = %s" @@ -57,8 +56,12 @@ class MediawikiService(BaseService): def add_user(self, username, password, **kwargs): """ Add a user """ + if 'user' in kwargs: + email = kwargs['user'].email + else: + email = '' pwhash = self._gen_mw_hash(password) - self._dbcursor.execute(self.SQL_ADD_USER, [self._clean_username(username), pwhash]) + self._dbcursor.execute(self.SQL_ADD_USER, [self._clean_username(username), pwhash, email]) self._db.connection.commit() return self._clean_username(username) From 4f5bc89d5ffe3fd88a1d99259a27ef9716ee0091 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Sun, 4 Apr 2010 01:07:44 +0100 Subject: [PATCH 071/116] Fixed race and gender description fuctions --- eve_api/models/api_player.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/eve_api/models/api_player.py b/eve_api/models/api_player.py index eb4f20f..528cc71 100644 --- a/eve_api/models/api_player.py +++ b/eve_api/models/api_player.py @@ -77,18 +77,16 @@ class EVEPlayerCharacter(EVEAPIModel): objects = EVEPlayerCharacterManager() @property - def gender_description(): - for va in API_GENDER_CHOICES: - k, v = va - if v == self.gender: - return k + def gender_description(self): + for choice in API_GENDER_CHOICES: + if choice[0] == self.gender: + return choice[1] @property - def race_description(): - for va in API_RACES_CHOICES: - k, v = va - if v == self.race: - return k + def race_description(self): + for choice in API_RACES_CHOICES: + if choice[0] == self.race: + return choice[1] def __unicode__(self): if self.name: From 82dffb13246fbf499e16561db50a66a97b9a0fd5 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Sun, 4 Apr 2010 01:09:06 +0100 Subject: [PATCH 072/116] Further work on issue #27, also enforced utf8 for XML decoding --- eve_api/api_puller/accounts.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/eve_api/api_puller/accounts.py b/eve_api/api_puller/accounts.py index 4be73b1..5a3c8af 100755 --- a/eve_api/api_puller/accounts.py +++ b/eve_api/api_puller/accounts.py @@ -27,7 +27,7 @@ def import_eve_account(api_key, user_id): params=auth_params, no_cache=False) - dom = minidom.parseString(account_doc.body) + dom = minidom.parseString(account_doc.body.encode('utf-8')) if dom.getElementsByTagName('error'): try: @@ -36,6 +36,7 @@ def import_eve_account(api_key, user_id): return account.api_status = API_STATUS_OTHER_ERROR + account.api_last_updated = datetime.utcnow() account.save() return @@ -50,8 +51,8 @@ def import_eve_account(api_key, user_id): account.api_key = api_key account.api_user_id = user_id - account.api_last_updated = datetime.utcnow() account.api_status = API_STATUS_OK + account.api_last_updated = datetime.utcnow() account.save() for node in characters_node_children: From a9e9497bfb87ec90ed2dc8ee47c1945bbc04b899 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Sun, 4 Apr 2010 01:33:26 +0100 Subject: [PATCH 073/116] Fixes Issue #27, woohoo! --- eve_api/admin.py | 7 +------ eve_api/api_puller/accounts.py | 25 ++++++++++++++++++++++--- eve_api/cron.py | 8 +------- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/eve_api/admin.py b/eve_api/admin.py index 17363cc..b068eca 100644 --- a/eve_api/admin.py +++ b/eve_api/admin.py @@ -8,12 +8,7 @@ from eve_api.api_puller.accounts import import_eve_account def account_api_update(modeladmin, request, queryset): for obj in queryset: - try: - import_eve_account(obj.api_key, obj.api_user_id) - obj.api_status = 1 - except APIAuthException: - obj.api_status = 2 - obj.save() + import_eve_account(obj.api_key, obj.api_user_id) account_api_update.short_description = "Update account from the EVE API" diff --git a/eve_api/api_puller/accounts.py b/eve_api/api_puller/accounts.py index 5a3c8af..8104431 100755 --- a/eve_api/api_puller/accounts.py +++ b/eve_api/api_puller/accounts.py @@ -23,9 +23,27 @@ def import_eve_account(api_key, user_id): Imports an account from the API into the EVEAccount model. """ auth_params = {'userID': user_id, 'apiKey': api_key} - account_doc = CachedDocument.objects.api_query('/account/Characters.xml.aspx', + + try: + account_doc = CachedDocument.objects.api_query('/account/Characters.xml.aspx', params=auth_params, no_cache=False) + except APIAuthException: + try: + account = EVEAccount.objects.get(id=user_id) + except EVEAccount.DoesNotExist: + return + if api_key == account.api_key: + account.api_status = API_STATUS_AUTH_ERROR + account.api_last_updated = datetime.utcnow() + account.save() + return + except APINoUserIDException: + try: + account = EVEAccount.objects.get(id=user_id) + account.delete() + except EVEAccount.DoesNotExist: + return dom = minidom.parseString(account_doc.body.encode('utf-8')) @@ -52,8 +70,6 @@ def import_eve_account(api_key, user_id): account.api_key = api_key account.api_user_id = user_id account.api_status = API_STATUS_OK - account.api_last_updated = datetime.utcnow() - account.save() for node in characters_node_children: try: @@ -63,6 +79,9 @@ def import_eve_account(api_key, user_id): except AttributeError: # This must be a Text node, ignore it. continue + + account.api_last_updated = datetime.utcnow() + account.save() return account def import_eve_character(api_key, user_id, character_id): diff --git a/eve_api/cron.py b/eve_api/cron.py index 26f70dc..2713072 100644 --- a/eve_api/cron.py +++ b/eve_api/cron.py @@ -24,13 +24,7 @@ class UpdateAPIs(): if not acc.user: acc.delete() continue - try: - eve_api.api_puller.accounts.import_eve_account(acc.api_key, acc.api_user_id) - acc.api_status = 1 - except APIAuthException: - acc.api_status = 2 - - acc.save() + eve_api.api_puller.accounts.import_eve_account(acc.api_key, acc.api_user_id) if self.settings['update_corp']: for corp in EVEPlayerCorporation.objects.all(): From 1fc325e0606e2c5da690f60222f5fd9f344a3c8d Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Sun, 4 Apr 2010 01:39:16 +0100 Subject: [PATCH 074/116] Now only updates API keys not updated in the last day --- eve_api/cron.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/eve_api/cron.py b/eve_api/cron.py index 2713072..37fb9f9 100644 --- a/eve_api/cron.py +++ b/eve_api/cron.py @@ -1,4 +1,5 @@ import logging +import datetime from eve_api.models.api_player import EVEAccount, EVEPlayerCorporation import eve_api.api_puller.accounts @@ -11,6 +12,8 @@ class UpdateAPIs(): settings = { 'update_corp': False } + last_update_delay = 86400 + @property def _logger(self): if not hasattr(self, '__logger'): @@ -19,7 +22,13 @@ class UpdateAPIs(): def job(self): # Update all the eve accounts and related corps - for acc in EVEAccount.objects.all(): + + delta = datetime.timedelta(seconds=self.last_update_delay) + self._logger.debug("Updating APIs older than %s" % (datetime.datetime.now() - delta)) + + accounts = EVEAccount.objects.filter(api_last_updated__lt=(datetime.datetime.now() - delta)) + self._logger.debug("%s account(s) to update" % len(accounts)) + for acc in accounts: self._logger.info("Updating UserID %s" % acc.api_user_id) if not acc.user: acc.delete() From a6415f8422f3c1d645c4a275cfa098abeedc553f Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Sun, 4 Apr 2010 01:39:49 +0100 Subject: [PATCH 075/116] Change API updates to run hourly --- cronjobs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cronjobs.txt b/cronjobs.txt index 8333f68..da657e3 100644 --- a/cronjobs.txt +++ b/cronjobs.txt @@ -2,5 +2,5 @@ ROOT=/home/matalok/auth/auth @daily $ROOT/run-cron.py reddit.cron UpdateAPIs */10 * * * * $ROOT/run-cron.py reddit.cron ProcessValidations -@daily $ROOT/run-cron.py eve_api.cron UpdateAPIs +@hourly $ROOT/run-cron.py eve_api.cron UpdateAPIs */10 * * * * $ROOT/run-cron.py sso.cron RemoveInvalidUsers > $ROOT/auth-update.log 2>&1 From e8ea6b9695e100eb6161b8517073c7e18683c84d Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Sun, 4 Apr 2010 01:48:07 +0100 Subject: [PATCH 076/116] Run Usercheck on the hour --- cronjobs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cronjobs.txt b/cronjobs.txt index da657e3..3486308 100644 --- a/cronjobs.txt +++ b/cronjobs.txt @@ -3,4 +3,4 @@ ROOT=/home/matalok/auth/auth @daily $ROOT/run-cron.py reddit.cron UpdateAPIs */10 * * * * $ROOT/run-cron.py reddit.cron ProcessValidations @hourly $ROOT/run-cron.py eve_api.cron UpdateAPIs -*/10 * * * * $ROOT/run-cron.py sso.cron RemoveInvalidUsers > $ROOT/auth-update.log 2>&1 +@hourly * * * * $ROOT/run-cron.py sso.cron RemoveInvalidUsers > $ROOT/auth-update.log 2>&1 From ca23d32eef823ef49532871af6d64149a01e6afa Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Sun, 4 Apr 2010 01:54:13 +0100 Subject: [PATCH 077/116] Batches eve_api updates in batches of 50, allows for gradual updates throughout the day --- cronjobs.txt | 6 +++--- eve_api/cron.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cronjobs.txt b/cronjobs.txt index 3486308..24edaad 100644 --- a/cronjobs.txt +++ b/cronjobs.txt @@ -1,6 +1,6 @@ ROOT=/home/matalok/auth/auth -@daily $ROOT/run-cron.py reddit.cron UpdateAPIs +@daily $ROOT/run-cron.py reddit.cron UpdateAPIs */10 * * * * $ROOT/run-cron.py reddit.cron ProcessValidations -@hourly $ROOT/run-cron.py eve_api.cron UpdateAPIs -@hourly * * * * $ROOT/run-cron.py sso.cron RemoveInvalidUsers > $ROOT/auth-update.log 2>&1 +*/5 * * * * $ROOT/run-cron.py eve_api.cron UpdateAPIs +@hourly $ROOT/run-cron.py sso.cron RemoveInvalidUsers > $ROOT/auth-update.log 2>&1 diff --git a/eve_api/cron.py b/eve_api/cron.py index 37fb9f9..ab90b4e 100644 --- a/eve_api/cron.py +++ b/eve_api/cron.py @@ -13,6 +13,7 @@ class UpdateAPIs(): settings = { 'update_corp': False } last_update_delay = 86400 + batches = 50 @property def _logger(self): @@ -26,7 +27,7 @@ class UpdateAPIs(): delta = datetime.timedelta(seconds=self.last_update_delay) self._logger.debug("Updating APIs older than %s" % (datetime.datetime.now() - delta)) - accounts = EVEAccount.objects.filter(api_last_updated__lt=(datetime.datetime.now() - delta)) + accounts = EVEAccount.objects.filter(api_last_updated__lt=(datetime.datetime.now() - delta))[:batches] self._logger.debug("%s account(s) to update" % len(accounts)) for acc in accounts: self._logger.info("Updating UserID %s" % acc.api_user_id) From c5e9cf99c2d845e976a2391740a76253e1eeae39 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Sun, 4 Apr 2010 01:55:24 +0100 Subject: [PATCH 078/116] Fixes batches issue --- eve_api/cron.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eve_api/cron.py b/eve_api/cron.py index ab90b4e..44e429d 100644 --- a/eve_api/cron.py +++ b/eve_api/cron.py @@ -27,7 +27,7 @@ class UpdateAPIs(): delta = datetime.timedelta(seconds=self.last_update_delay) self._logger.debug("Updating APIs older than %s" % (datetime.datetime.now() - delta)) - accounts = EVEAccount.objects.filter(api_last_updated__lt=(datetime.datetime.now() - delta))[:batches] + accounts = EVEAccount.objects.filter(api_last_updated__lt=(datetime.datetime.now() - delta))[:self.batches] self._logger.debug("%s account(s) to update" % len(accounts)) for acc in accounts: self._logger.info("Updating UserID %s" % acc.api_user_id) From 778544e4e8a89e7c6db7e440487e30c772646475 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Sun, 4 Apr 2010 02:04:32 +0100 Subject: [PATCH 079/116] Updated crontab and added logs folder to ignore --- .gitignore | 1 + cronjobs.txt | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 831e1fc..db1d622 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ django/ registration/ dbsettings.py django_evolution +logs/ diff --git a/cronjobs.txt b/cronjobs.txt index 24edaad..85460ad 100644 --- a/cronjobs.txt +++ b/cronjobs.txt @@ -1,6 +1,6 @@ ROOT=/home/matalok/auth/auth -@daily $ROOT/run-cron.py reddit.cron UpdateAPIs -*/10 * * * * $ROOT/run-cron.py reddit.cron ProcessValidations -*/5 * * * * $ROOT/run-cron.py eve_api.cron UpdateAPIs -@hourly $ROOT/run-cron.py sso.cron RemoveInvalidUsers > $ROOT/auth-update.log 2>&1 +@daily $ROOT/run-cron.py reddit.cron UpdateAPIs > $ROOT/logs/redditapi-update.log 2>&1 +*/10 * * * * $ROOT/run-cron.py reddit.cron ProcessValidations > $ROOT/logs/reddit-validations.log 2>&1 +*/5 * * * * * $ROOT/run-cron.py eve_api.cron UpdateAPIs > $ROOT/logs/eveapi-update.log 2>&1 +@hourly $ROOT/run-cron.py sso.cron RemoveInvalidUsers > $ROOT/logs/auth-update.log 2>&1 From 98adcf8f6c521492349633b87568a641fb1b8c9f Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Sun, 4 Apr 2010 02:05:22 +0100 Subject: [PATCH 080/116] Fixed cronjobs --- cronjobs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cronjobs.txt b/cronjobs.txt index 85460ad..9fc9cec 100644 --- a/cronjobs.txt +++ b/cronjobs.txt @@ -2,5 +2,5 @@ ROOT=/home/matalok/auth/auth @daily $ROOT/run-cron.py reddit.cron UpdateAPIs > $ROOT/logs/redditapi-update.log 2>&1 */10 * * * * $ROOT/run-cron.py reddit.cron ProcessValidations > $ROOT/logs/reddit-validations.log 2>&1 -*/5 * * * * * $ROOT/run-cron.py eve_api.cron UpdateAPIs > $ROOT/logs/eveapi-update.log 2>&1 +*/5 * * * * $ROOT/run-cron.py eve_api.cron UpdateAPIs > $ROOT/logs/eveapi-update.log 2>&1 @hourly $ROOT/run-cron.py sso.cron RemoveInvalidUsers > $ROOT/logs/auth-update.log 2>&1 From c77d0c948e55e50921755fc67893d948bf9edc79 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Sun, 4 Apr 2010 01:33:25 +0100 Subject: [PATCH 081/116] Identifies error 211 and flags it diff --- eve_api/api_puller/accounts.py | 10 ++++++++-- eve_api/app_defines.py | 2 ++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/eve_api/api_puller/accounts.py b/eve_api/api_puller/accounts.py index 8104431..d247dcd 100755 --- a/eve_api/api_puller/accounts.py +++ b/eve_api/api_puller/accounts.py @@ -47,13 +47,19 @@ def import_eve_account(api_key, user_id): dom = minidom.parseString(account_doc.body.encode('utf-8')) - if dom.getElementsByTagName('error'): + enode = dom.getElementsByTagName('error') + print enode + if enode: try: account = EVEAccount.objects.get(id=user_id) except EVEAccount.DoesNotExist: return - account.api_status = API_STATUS_OTHER_ERROR + error = enode[0].getAttribute('code') + if error == '211': + account.api_status = API_STATUS_ACC_EXPIRED + else: + account.api_status = API_STATUS_OTHER_ERROR account.api_last_updated = datetime.utcnow() account.save() return diff --git a/eve_api/app_defines.py b/eve_api/app_defines.py index 2356d04..48e5905 100644 --- a/eve_api/app_defines.py +++ b/eve_api/app_defines.py @@ -6,12 +6,14 @@ API_STATUS_PENDING = 0 API_STATUS_OK = 1 API_STATUS_AUTH_ERROR = 2 API_STATUS_OTHER_ERROR = 3 +API_STATUS_ACC_EXPIRED = 4 # This tuple is used to assemble the choices list for the field. API_STATUS_CHOICES = ( (API_STATUS_PENDING, 'Unknown'), (API_STATUS_OK, 'OK'), (API_STATUS_AUTH_ERROR, 'Auth Error'), (API_STATUS_OTHER_ERROR, 'Other Error'), + (API_STATUS_ACC_EXPIRED, 'Account Expired'), ) API_GENDER_CHOICES = ( From bd0e292c341b6fc336d51186b042c63ed28100e2 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Sun, 4 Apr 2010 01:56:21 +0100 Subject: [PATCH 082/116] Removed debug line --- eve_api/api_puller/accounts.py | 1 - 1 file changed, 1 deletion(-) diff --git a/eve_api/api_puller/accounts.py b/eve_api/api_puller/accounts.py index d247dcd..c95ec37 100755 --- a/eve_api/api_puller/accounts.py +++ b/eve_api/api_puller/accounts.py @@ -48,7 +48,6 @@ def import_eve_account(api_key, user_id): dom = minidom.parseString(account_doc.body.encode('utf-8')) enode = dom.getElementsByTagName('error') - print enode if enode: try: account = EVEAccount.objects.get(id=user_id) From 6b8a4e374da2b64960d6f28706f43215f68515a2 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Mon, 5 Apr 2010 02:42:12 +0100 Subject: [PATCH 083/116] Added applications field into EVEPlayerCorporation --- eve_api/evolutions/__init__.py | 1 + eve_api/evolutions/applications-field.py | 7 +++++++ eve_api/models/api_player.py | 1 + 3 files changed, 9 insertions(+) create mode 100644 eve_api/evolutions/__init__.py create mode 100644 eve_api/evolutions/applications-field.py diff --git a/eve_api/evolutions/__init__.py b/eve_api/evolutions/__init__.py new file mode 100644 index 0000000..0fdfe86 --- /dev/null +++ b/eve_api/evolutions/__init__.py @@ -0,0 +1 @@ +SEQUENCE = ['applications-field'] diff --git a/eve_api/evolutions/applications-field.py b/eve_api/evolutions/applications-field.py new file mode 100644 index 0000000..9616b5a --- /dev/null +++ b/eve_api/evolutions/applications-field.py @@ -0,0 +1,7 @@ +from django_evolution.mutations import * +from django.db import models + +MUTATIONS = [ + AddField('EVEPlayerCorporation', 'applications', models.BooleanField, initial=False) +] + diff --git a/eve_api/models/api_player.py b/eve_api/models/api_player.py index 528cc71..5037a8a 100644 --- a/eve_api/models/api_player.py +++ b/eve_api/models/api_player.py @@ -157,6 +157,7 @@ class EVEPlayerCorporation(EVEAPIModel): logo_color3 = models.IntegerField(blank=True, null=True) group = models.ForeignKey(Group, blank=True, null=True) + applications = models.BooleanField(blank=False, default=False) objects = EVEPlayerCorporationManager() From 52a41f42290b5dbb37ac937078b71911a4b6ca87 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Mon, 5 Apr 2010 02:43:27 +0100 Subject: [PATCH 084/116] Initial commit of the HR management code --- hr/__init__.py | 0 hr/admin.py | 17 ++++ hr/app_defines.py | 16 +++ hr/forms.py | 34 +++++++ hr/models.py | 40 ++++++++ hr/tests.py | 23 +++++ hr/urls.py | 14 +++ hr/views.py | 102 ++++++++++++++++++++ templates/base.html | 23 ++--- templates/hr/applications/add.html | 16 +++ templates/hr/applications/view_list.html | 26 +++++ templates/hr/index.html | 22 +++++ templates/hr/recommendations/add.html | 16 +++ templates/hr/recommendations/view_list.html | 26 +++++ 14 files changed, 364 insertions(+), 11 deletions(-) create mode 100644 hr/__init__.py create mode 100644 hr/admin.py create mode 100644 hr/app_defines.py create mode 100644 hr/forms.py create mode 100644 hr/models.py create mode 100644 hr/tests.py create mode 100644 hr/urls.py create mode 100644 hr/views.py create mode 100644 templates/hr/applications/add.html create mode 100644 templates/hr/applications/view_list.html create mode 100644 templates/hr/index.html create mode 100644 templates/hr/recommendations/add.html create mode 100644 templates/hr/recommendations/view_list.html diff --git a/hr/__init__.py b/hr/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hr/admin.py b/hr/admin.py new file mode 100644 index 0000000..18dad44 --- /dev/null +++ b/hr/admin.py @@ -0,0 +1,17 @@ +from django.contrib import admin +from django.contrib.auth.models import User +from django.contrib.auth.admin import UserAdmin +from hr.models import Application, Recommendation + +class ApplicationAdmin(admin.ModelAdmin): + list_display = ('user', 'character', 'status') + search_fields = ['user', 'character', 'status'] + +admin.site.register(Application, ApplicationAdmin) + +class RecommendationAdmin(admin.ModelAdmin): + list_display = ('user', 'user_character') + search_fields = ['user_character'] + +admin.site.register(Recommendation, RecommendationAdmin) + diff --git a/hr/app_defines.py b/hr/app_defines.py new file mode 100644 index 0000000..60c81d0 --- /dev/null +++ b/hr/app_defines.py @@ -0,0 +1,16 @@ + +APPLICATION_STATUS_NOTSUBMITTED = 0 +APPLICATION_STATUS_AWAITINGREVIEW = 1 +APPLICATION_STATUS_REJECTED = 2 +APPLICATION_STATUS_ACCEPTED = 3 +APPLICATION_STATUS_QUERY = 4 +APPLICATION_STATUS_COMPLETED = 5 + +APPLICATION_STATUS_CHOICES = ( + (APPLICATION_STATUS_NOTSUBMITTED, 'Not Submitted'), + (APPLICATION_STATUS_AWAITINGREVIEW, 'Awaiting Review'), + (APPLICATION_STATUS_REJECTED, 'Rejected'), + (APPLICATION_STATUS_ACCEPTED, 'Accepted'), + (APPLICATION_STATUS_QUERY, 'In Query'), + (APPLICATION_STATUS_COMPLETED, 'Completed'), +) diff --git a/hr/forms.py b/hr/forms.py new file mode 100644 index 0000000..e3be592 --- /dev/null +++ b/hr/forms.py @@ -0,0 +1,34 @@ +from django import forms +import settings + +from hr.app_defines import * +from hr.models import Application +from eve_api.models import EVEPlayerCharacter + +def CreateRecommendationForm(user): + """ Generate a Recommendation form based on the user's permissions """ + + characters = EVEPlayerCharacter.objects.filter(eveaccount__user=user) + applications = Application.objects.filter(status=APPLICATION_STATUS_NOTSUBMITTED) + + class RecommendationForm(forms.Form): + """ Service Account Form """ + + character = forms.ModelChoiceField(queryset=characters, required=True, empty_label=None) + application = forms.ModelChoiceField(queryset=applications, required=True, empty_label=None) + + return RecommendationForm + + +def CreateApplicationForm(user): + """ Generate a Application form based on the user's permissions """ + + characters = EVEPlayerCharacter.objects.filter(eveaccount__user=user) + corporations = EVEPlayerCorporation.objects.filter(applications=True) + + class ApplicationForm(forms.Form): + character = forms.ModelChoiceField(queryset=characters, required=True, empty_label=None) + + return ApplicationForm + + diff --git a/hr/models.py b/hr/models.py new file mode 100644 index 0000000..66ae9a6 --- /dev/null +++ b/hr/models.py @@ -0,0 +1,40 @@ +from datetime import datetime + +from django.db import models +from django.contrib.auth.models import User + +from eve_api.models import EVEPlayerCharacter, EVEPlayerCorporation + +from hr.app_defines import * + +class Application(models.Model): + user = models.ForeignKey(User, blank=False, verbose_name="User") + character = models.ForeignKey(EVEPlayerCharacter, blank=False, verbose_name="Character") + corporation = models.ForeignKey(EVEPlayerCorporation, blank=False, verbose_name="Applying to Corporation") + status = models.IntegerField(choices=APPLICATION_STATUS_CHOICES, + default=APPLICATION_STATUS_NOTSUBMITTED, + verbose_name="Status", + help_text="Current status of this application request.") + + @property + def status_description(self): + for choice in APPLICATION_STATUS_CHOICES: + if choice[0] == self.status: + return choice[1] + + def __unicode__(self): + return self.character.name + + def __str__(self): + return self.__unicode__() + +class Recommendation(models.Model): + user = models.ForeignKey(User, blank=False, verbose_name="User") + user_character = models.ForeignKey(EVEPlayerCharacter, blank=False, verbose_name="Recommender") + application = models.ForeignKey(Application, blank=False, verbose_name="Recommended Application") + + def __unicode__(self): + return self.user_character.name + + def __str__(self): + return self.__unicode__() diff --git a/hr/tests.py b/hr/tests.py new file mode 100644 index 0000000..2247054 --- /dev/null +++ b/hr/tests.py @@ -0,0 +1,23 @@ +""" +This file demonstrates two different styles of tests (one doctest and one +unittest). These will both pass when you run "manage.py test". + +Replace these with more appropriate tests for your application. +""" + +from django.test import TestCase + +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.failUnlessEqual(1 + 1, 2) + +__test__ = {"doctest": """ +Another way to test that 1 + 1 is equal to 2. + +>>> 1 + 1 == 2 +True +"""} + diff --git a/hr/urls.py b/hr/urls.py new file mode 100644 index 0000000..68cd0c9 --- /dev/null +++ b/hr/urls.py @@ -0,0 +1,14 @@ +from django.conf.urls.defaults import * + +from hr import views + +urlpatterns = patterns('', + ('^$', views.index), + (r'^recommendations/$', views.view_recommendations), + (r'^recommendations/(?P.*)/$', views.view_recommendation), + (r'^applications/$', views.view_applications), + (r'^applications/(?P.*)/$', views.view_application), + + (r'^add/application/$', views.add_application), + (r'^add/recommendation/$', views.add_recommendation), +) diff --git a/hr/views.py b/hr/views.py new file mode 100644 index 0000000..c622a7a --- /dev/null +++ b/hr/views.py @@ -0,0 +1,102 @@ +import datetime + +from django.http import HttpResponseRedirect +from django.shortcuts import render_to_response +from django.core.urlresolvers import reverse +from django.contrib.auth.models import User +from django.contrib.auth.decorators import login_required +from django.template import RequestContext + +from hr.forms import CreateRecommendationForm, CreateApplicationForm +from hr.models import Recommendation, Application + + +def index(request): + if request.user.is_staff or 'hrstaff' in request.user.groups.all(): + hrstaff = True + + return render_to_response('hr/index.html', locals(), context_instance=RequestContext(request)) + +### Application Management + +@login_required +def view_applications(request): + apps = Application.objects.filter(user=request.user) + return render_to_response('hr/applications/view_list.html', locals(), context_instance=RequestContext(request)) + +@login_required +def view_application(request, applicationid): + try: + app = Application.objects.get(id=applicationid) + except Application.DoesNotExist: + return HttpResponseRedirect(reverse('hr.views.index')) + return render_to_response('hr/applications/view.html', locals(), context_instance=RequestContext(request)) + +@login_required +def add_application(request): + + clsform = CreateApplicationForm(request.user) + if request.method == 'POST': + form = clsform(request.POST) + if form.is_valid(): + app = Application() + + app.user = request.user + app.character = form.cleaned_data['character'] + app.corporation = form.cleaned_data['corporation'] + app.save() + + request.user.message_set.create(message="Application has been submitted." % rec.application ) + return HttpResponseRedirect(reverse('hr.views.view_applications')) + else: + return HttpResponseRedirect(reverse('hr.views.add_application')) + + else: + form = clsform() # An unbound form + + return render_to_response('hr/applications/add.html', locals(), context_instance=RequestContext(request)) + +### Recommendation Management + +@login_required +def view_recommendations(request): + recs = Recommendation.objects.filter(user=request.user, application__status=0) + return render_to_response('hr/recommendations/view_list.html', locals(), context_instance=RequestContext(request)) + +@login_required +def view_recommendation(request, recommendationid): + try: + rec = Recommendation.objects.get(id=recommendationid, user=request.user) + except Recommendation.DoesNotExist: + return HttpResponseRedirect(reverse('hr.views.index')) + return render_to_response('hr/recommendations/view.html', locals(), context_instance=RequestContext(request)) + +@login_required +def add_recommendation(request): + + clsform = CreateRecommendationForm(request.user) + + if request.method == 'POST': + form = clsform(request.POST) + if form.is_valid(): + rec = Recommendation() + + rec.user = request.user + rec.user_character = form.cleaned_data['character'] + rec.application = form.cleaned_data['application'] + rec.created_by = request.user + rec.last_updated_by = request.user + rec.save() + + request.user.message_set.create(message="Recommendation added to %s's application" % rec.application ) + return HttpResponseRedirect(reverse('hr.views.view_recommendations')) + else: + return HttpResponseRedirect(reverse('hr.views.add_recommendation')) + + else: + form = clsform() # An unbound form + + return render_to_response('hr/recommendations/add.html', locals(), context_instance=RequestContext(request)) + + + diff --git a/templates/base.html b/templates/base.html index 762855c..7c8b1b0 100644 --- a/templates/base.html +++ b/templates/base.html @@ -15,17 +15,18 @@
diff --git a/templates/hr/applications/add.html b/templates/hr/applications/add.html new file mode 100644 index 0000000..4a14c5c --- /dev/null +++ b/templates/hr/applications/add.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block title %}Add EVE API Key{% endblock %} + +{% block content %} +

Select a character you wish to recommend from, then select your friend's current application. + +The person you are recommending needs to have created their application before you can add a recommendation.

+ +
+ +{{ form.as_table }} +
+ +
+{% endblock %} diff --git a/templates/hr/applications/view_list.html b/templates/hr/applications/view_list.html new file mode 100644 index 0000000..743d700 --- /dev/null +++ b/templates/hr/applications/view_list.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} + +{% block title %}Recommendations{% endblock %} + +{% block content %} +

This list shows your current open recommendations that are yet to be submitted, as +soon as the recommended user submits their application your recommendation will be removed from this list.

+{% if recs %} + + + + + +{% for rec in recs %} + + + + + +{% endfor %} +
RecommenderRecommended ApplicationApplication Status
{{ rec.user_character }}{{ rec.application }}{{ rec.application.status_description }}
+{% else %} +

You have no current recommendations

+{% endif %} + +{% endblock %} diff --git a/templates/hr/index.html b/templates/hr/index.html new file mode 100644 index 0000000..f5cc749 --- /dev/null +++ b/templates/hr/index.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} + +{% block title %}HR{% endblock %} + +{% block content %} +

Applications

+

View your current open applications
+Create a application

+

Recommendations

+

+View your current open recommendations
+Add a recommendation
+

+ +{% if hrstaff %} +

HR Admin

+

+View applications
+

+{% endif %} + +{% endblock %} diff --git a/templates/hr/recommendations/add.html b/templates/hr/recommendations/add.html new file mode 100644 index 0000000..4a14c5c --- /dev/null +++ b/templates/hr/recommendations/add.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block title %}Add EVE API Key{% endblock %} + +{% block content %} +

Select a character you wish to recommend from, then select your friend's current application. + +The person you are recommending needs to have created their application before you can add a recommendation.

+ +
+ +{{ form.as_table }} +
+ +
+{% endblock %} diff --git a/templates/hr/recommendations/view_list.html b/templates/hr/recommendations/view_list.html new file mode 100644 index 0000000..743d700 --- /dev/null +++ b/templates/hr/recommendations/view_list.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} + +{% block title %}Recommendations{% endblock %} + +{% block content %} +

This list shows your current open recommendations that are yet to be submitted, as +soon as the recommended user submits their application your recommendation will be removed from this list.

+{% if recs %} + + + + + +{% for rec in recs %} + + + + + +{% endfor %} +
RecommenderRecommended ApplicationApplication Status
{{ rec.user_character }}{{ rec.application }}{{ rec.application.status_description }}
+{% else %} +

You have no current recommendations

+{% endif %} + +{% endblock %} From 956052c25cb482661daf84c6d4f4e3476a36ed32 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Mon, 5 Apr 2010 02:53:27 +0100 Subject: [PATCH 085/116] If a user is loggged in, redirect to the profile page --- sso/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sso/views.py b/sso/views.py index 247d08a..9eef356 100644 --- a/sso/views.py +++ b/sso/views.py @@ -20,7 +20,10 @@ from reddit.models import RedditAccount import settings def index(request): - return render_to_response('sso/index.html', context_instance=RequestContext(request)) + if request.user: + return HttpResponseRedirect(reverse('sso.views.profile')) + else: + return render_to_response('sso/index.html', context_instance=RequestContext(request)) @login_required def profile(request): From 586a2727b119088cffe6f70cba4693972a013845 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Mon, 5 Apr 2010 03:10:15 +0100 Subject: [PATCH 086/116] Various fixes and work on the HR application. --- hr/forms.py | 8 +++++++- hr/views.py | 13 +++++++------ templates/hr/applications/add.html | 4 ++-- templates/hr/applications/noadd.html | 7 +++++++ templates/hr/applications/view_list.html | 24 ++++++++++-------------- 5 files changed, 33 insertions(+), 23 deletions(-) create mode 100644 templates/hr/applications/noadd.html diff --git a/hr/forms.py b/hr/forms.py index e3be592..7134da6 100644 --- a/hr/forms.py +++ b/hr/forms.py @@ -3,7 +3,7 @@ import settings from hr.app_defines import * from hr.models import Application -from eve_api.models import EVEPlayerCharacter +from eve_api.models import EVEPlayerCharacter, EVEPlayerCorporation def CreateRecommendationForm(user): """ Generate a Recommendation form based on the user's permissions """ @@ -28,7 +28,13 @@ def CreateApplicationForm(user): class ApplicationForm(forms.Form): character = forms.ModelChoiceField(queryset=characters, required=True, empty_label=None) + corporation = forms.ModelChoiceField(queryset=corporations, required=True, empty_label=None) + def clean(self): + if len(Application.objects.filter(character=self.cleaned_data['character'], status__in=[APPLICATION_STATUS_NOTSUBMITTED, APPLICATION_STATUS_AWAITINGREVIEW, APPLICATION_STATUS_QUERY])): + raise forms.ValidationError("This character already has a open application") + + return self.cleaned_data return ApplicationForm diff --git a/hr/views.py b/hr/views.py index c622a7a..c53f8fa 100644 --- a/hr/views.py +++ b/hr/views.py @@ -7,6 +7,8 @@ from django.contrib.auth.models import User from django.contrib.auth.decorators import login_required from django.template import RequestContext +from eve_api.models import EVEPlayerCorporation + from hr.forms import CreateRecommendationForm, CreateApplicationForm from hr.models import Recommendation, Application @@ -46,15 +48,16 @@ def add_application(request): app.corporation = form.cleaned_data['corporation'] app.save() - request.user.message_set.create(message="Application has been submitted." % rec.application ) + request.user.message_set.create(message="Your application to %s has been submitted." % app.corporation) return HttpResponseRedirect(reverse('hr.views.view_applications')) - else: - return HttpResponseRedirect(reverse('hr.views.add_application')) else: form = clsform() # An unbound form - return render_to_response('hr/applications/add.html', locals(), context_instance=RequestContext(request)) + if len(EVEPlayerCorporation.objects.filter(applications=True)): + return render_to_response('hr/applications/add.html', locals(), context_instance=RequestContext(request)) + else: + return render_to_response('hr/applications/noadd.html', locals(), context_instance=RequestContext(request)) ### Recommendation Management @@ -90,8 +93,6 @@ def add_recommendation(request): request.user.message_set.create(message="Recommendation added to %s's application" % rec.application ) return HttpResponseRedirect(reverse('hr.views.view_recommendations')) - else: - return HttpResponseRedirect(reverse('hr.views.add_recommendation')) else: form = clsform() # An unbound form diff --git a/templates/hr/applications/add.html b/templates/hr/applications/add.html index 4a14c5c..2447ff3 100644 --- a/templates/hr/applications/add.html +++ b/templates/hr/applications/add.html @@ -7,10 +7,10 @@ The person you are recommending needs to have created their application before you can add a recommendation.

-
+ {{ form.as_table }}
- +
{% endblock %} diff --git a/templates/hr/applications/noadd.html b/templates/hr/applications/noadd.html new file mode 100644 index 0000000..24782e9 --- /dev/null +++ b/templates/hr/applications/noadd.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} + +{% block title %}Create Application{% endblock %} + +{% block content %} +

Unfortunatly, no Corporations are accepting applications at the moment.

+{% endblock %} diff --git a/templates/hr/applications/view_list.html b/templates/hr/applications/view_list.html index 743d700..ffc2c57 100644 --- a/templates/hr/applications/view_list.html +++ b/templates/hr/applications/view_list.html @@ -1,26 +1,22 @@ {% extends "base.html" %} -{% block title %}Recommendations{% endblock %} +{% block title %}Applications{% endblock %} {% block content %} -

This list shows your current open recommendations that are yet to be submitted, as -soon as the recommended user submits their application your recommendation will be removed from this list.

-{% if recs %} +

This list shows your current open applications

+{% if apps %} - - - - -{% for rec in recs %} - - - + +{% for app in apps %} + + + + - {% endfor %}
RecommenderRecommended ApplicationApplication Status
{{ rec.user_character }}{{ rec.application }}{{ rec.application.status_description }}
Application IDCharacterCorporationApplication Status
{{ app.id }}{{ app.character }}{{ app.corporation }}{{ app.status_description }}
{% else %} -

You have no current recommendations

+

You have no current applications

{% endif %} {% endblock %} From 87b4b01ec329500de62c8e83dbdfbe7afb960690 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Mon, 5 Apr 2010 03:19:29 +0100 Subject: [PATCH 087/116] Added templatetag to only display enabled apps --- sso/templatetags/__init__.py | 0 sso/templatetags/installed.py | 20 ++++++++++++++++++++ templates/base.html | 4 ++++ 3 files changed, 24 insertions(+) create mode 100644 sso/templatetags/__init__.py create mode 100644 sso/templatetags/installed.py diff --git a/sso/templatetags/__init__.py b/sso/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sso/templatetags/installed.py b/sso/templatetags/installed.py new file mode 100644 index 0000000..01a68a7 --- /dev/null +++ b/sso/templatetags/installed.py @@ -0,0 +1,20 @@ +from django import template +from django.conf import settings +from django.template.defaultfilters import stringfilter + +register = template.Library() + +@register.filter +@stringfilter +def installed(value): + apps = settings.INSTALLED_APPS + if "." in value: + for app in apps: + if app == value: + return True + else: + for app in apps: + fields = app.split(".") + if fields[-1] == value: + return True + return False diff --git a/templates/base.html b/templates/base.html index 7c8b1b0..9de6e3c 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,3 +1,5 @@ +{% load installed %} + @@ -17,7 +19,9 @@
  • Home
  • {% if request.user %}
  • Profile
  • + {% if "hr"|installed %}
  • HR
  • + {% endif %}
  • Characters
  • {% if request.user.is_staff %}
  • Lookup User
  • From 7980e4bcb4704890c9230cbc7c0d6dbbc49f1873 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Mon, 5 Apr 2010 03:20:11 +0100 Subject: [PATCH 088/116] Added HR to urls and added a disabled entry in settings.py --- settings.py | 1 + urls.py | 1 + 2 files changed, 2 insertions(+) diff --git a/settings.py b/settings.py index 3ef8565..0f0dd26 100644 --- a/settings.py +++ b/settings.py @@ -79,6 +79,7 @@ INSTALLED_APPS = ( 'eve_api', 'mumble', 'reddit', +# 'hr', 'sso', ) diff --git a/urls.py b/urls.py index a9ce189..0a8a0e3 100644 --- a/urls.py +++ b/urls.py @@ -14,6 +14,7 @@ urlpatterns = patterns('', ('', include('registration.urls')), ('', include('sso.urls')), (r'^eveapi/', include('eve_proxy.urls')), + (r'^hr/', include('hr.urls')), ) urlpatterns += patterns('', From 2776ce8c165ccc33bb6860b83c53c7f1047267f4 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Mon, 5 Apr 2010 12:27:05 +0100 Subject: [PATCH 089/116] HR Group now a setting --- hr/views.py | 4 +++- settings.py | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/hr/views.py b/hr/views.py index c53f8fa..255c35d 100644 --- a/hr/views.py +++ b/hr/views.py @@ -7,6 +7,8 @@ from django.contrib.auth.models import User from django.contrib.auth.decorators import login_required from django.template import RequestContext +import settings + from eve_api.models import EVEPlayerCorporation from hr.forms import CreateRecommendationForm, CreateApplicationForm @@ -14,7 +16,7 @@ from hr.models import Recommendation, Application def index(request): - if request.user.is_staff or 'hrstaff' in request.user.groups.all(): + if request.user.is_staff or settings.HR_STAFF_GROUP in request.user.groups.all(): hrstaff = True return render_to_response('hr/index.html', locals(), context_instance=RequestContext(request)) diff --git a/settings.py b/settings.py index 0f0dd26..b75c003 100644 --- a/settings.py +++ b/settings.py @@ -141,3 +141,7 @@ MINING_DATABASE = 'dreddit_mining' # Mining buddy secret key (in the config) MINING_SALT = 's98ss7fsc7fd2rf62ctcrlwztstnzve9toezexcsdhfgviuinusxcdtsvbrg' +### HR Settings + +HR_STAFF_GROUP = 'hrstaff' + From 53165895b0914c5cdf69f048652b7898b8247375 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Mon, 5 Apr 2010 19:23:43 +0100 Subject: [PATCH 090/116] Added recent_posts function to pull recent posts/comments from API --- reddit/models.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/reddit/models.py b/reddit/models.py index ee3e9aa..98838aa 100644 --- a/reddit/models.py +++ b/reddit/models.py @@ -36,6 +36,19 @@ class RedditAccount(models.Model): self.date_created = datetime.fromtimestamp(data['created_utc']) self.last_update = datetime.now() + def recent_posts(self): + try: + jsondoc = json.load(urllib.urlopen("http://reddit.com/user/%s.json" % self.username)) + except: + raise self.DoesNotExist + + posts = [] + for item in jsondoc['data']['children']: + posts.append(item['data']) + + return posts + + class Meta: app_label = 'reddit' ordering = ['username'] From a5adee539e7d37f48df394c58d8821f190dd7cca Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Mon, 5 Apr 2010 19:57:47 +0100 Subject: [PATCH 091/116] Added view application --- hr/views.py | 15 +++++++++- templates/hr/applications/view.html | 43 +++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 templates/hr/applications/view.html diff --git a/hr/views.py b/hr/views.py index 255c35d..a55e76a 100644 --- a/hr/views.py +++ b/hr/views.py @@ -9,7 +9,8 @@ from django.template import RequestContext import settings -from eve_api.models import EVEPlayerCorporation +from eve_api.models import EVEAccount, EVEPlayerCorporation +from reddit.models import RedditAccount from hr.forms import CreateRecommendationForm, CreateApplicationForm from hr.models import Recommendation, Application @@ -34,6 +35,18 @@ def view_application(request, applicationid): app = Application.objects.get(id=applicationid) except Application.DoesNotExist: return HttpResponseRedirect(reverse('hr.views.index')) + + if not app.user == request.user and not (request.user.is_staff or settings.HR_STAFF_GROUP in request.user.groups.all()): + return HttpResponseRedirect(reverse('hr.views.index')) + + eveacc = EVEAccount.objects.filter(user=app.user) + redditacc = RedditAccount.objects.filter(user=app.user) + recs = Recommendation.objects.filter(application=app) + + posts = [] + for acc in redditacc: + posts.extend(acc.recent_posts()) + return render_to_response('hr/applications/view.html', locals(), context_instance=RequestContext(request)) @login_required diff --git a/templates/hr/applications/view.html b/templates/hr/applications/view.html new file mode 100644 index 0000000..6b3e93d --- /dev/null +++ b/templates/hr/applications/view.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} + +{% block title %}View Application{% endblock %} + +{% block content %} +

    Application Details

    + +
      +
    • Applying Auth User: {{ app.user }}
    • +
    • Applying Character: {{ app.character }}
    • +
    • Application Status: {{ app.status_description }}
    • +
    + +{% if recs %} +

    Recommendations

    +
      +{% for rec in recs %} +
    • {{ rec.user }} / {{ rec.user_character }} - {{ rec.user_character.corporation }}
    • +{% endfor %} +
    +{% endif %} + +

    EVE Characters

    +
      +{% for acc in eveacc %} +{% for char in acc.characters.all %} +
    • {{ char.name }} - {{ char.corporation }}/{{ char.corporation.alliance }}
    • +{% endfor %} +{% endfor %} +
    + +

    Reddit Posts

    +
      +{% for post in posts %} +{% if post.permalink %} +

      {{ post.title }} - /r/{{ post.subreddit }}

      +{% else %} +

      {{ post.body }} - Permalink

      +{% endif %} +{% endfor %} +
    + +{% endblock %} From 56ce23a186e518780a12107c3bb73c815d697ff3 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Mon, 5 Apr 2010 22:22:36 +0100 Subject: [PATCH 092/116] Fixed jabber disableuser function --- sso/services/jabber/xmppclient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sso/services/jabber/xmppclient.py b/sso/services/jabber/xmppclient.py index 2f0642c..844b7f4 100644 --- a/sso/services/jabber/xmppclient.py +++ b/sso/services/jabber/xmppclient.py @@ -127,7 +127,7 @@ class JabberAdmin(): except: return False - if self.resetpassword(username, hashlib.sha1('%s%s%s' % (username, random.randint(0, 2147483647))).hexdigest()): + if self.resetpassword(username, hashlib.sha1('%s%s' % (username, random.randint(0, 2147483647))).hexdigest()): return self.kickuser(username) else: return False From 5229d927c945a4c2d9883e5ab6f6a6ff5af3a2c2 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Mon, 5 Apr 2010 22:32:09 +0100 Subject: [PATCH 093/116] Now kicks users when account is disabled on Mumble --- sso/services/mumble/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sso/services/mumble/__init__.py b/sso/services/mumble/__init__.py index a8b56c3..a8c38a7 100644 --- a/sso/services/mumble/__init__.py +++ b/sso/services/mumble/__init__.py @@ -54,12 +54,19 @@ class MumbleService(BaseService): def disable_user(self, uid): """ Disable a user by uid """ + + srv = self._get_server() try: - mumbleuser = MumbleUser.objects.get(name=uid, server=self._get_server()) + mumbleuser = MumbleUser.objects.get(name=uid, server=srv) except MumbleUser.DoesNotExist: return False mumbleuser.password = "" mumbleuser.save() + + for session in srv.players: + userdtl = srv.players[session] + if userdtl.name = uid: + srv.kickUser(session, "Account Disabled") return True def enable_user(self, uid, password): From 94575a419bfc6f4671c5258eef7114a57b498d1f Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Mon, 5 Apr 2010 22:34:21 +0100 Subject: [PATCH 094/116] Fixed typo --- settings.py | 2 +- sso/services/mumble/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/settings.py b/settings.py index b75c003..bde9378 100644 --- a/settings.py +++ b/settings.py @@ -79,7 +79,7 @@ INSTALLED_APPS = ( 'eve_api', 'mumble', 'reddit', -# 'hr', + 'hr', 'sso', ) diff --git a/sso/services/mumble/__init__.py b/sso/services/mumble/__init__.py index a8c38a7..8d1df94 100644 --- a/sso/services/mumble/__init__.py +++ b/sso/services/mumble/__init__.py @@ -65,7 +65,7 @@ class MumbleService(BaseService): for session in srv.players: userdtl = srv.players[session] - if userdtl.name = uid: + if userdtl.name == uid: srv.kickUser(session, "Account Disabled") return True From fbc8ee8803f7c1c9d166336ee94ab8783dd484d8 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Mon, 5 Apr 2010 23:22:43 +0100 Subject: [PATCH 095/116] Fixed enable/disable --- sso/services/mumble/__init__.py | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/sso/services/mumble/__init__.py b/sso/services/mumble/__init__.py index 8d1df94..5f89e5a 100644 --- a/sso/services/mumble/__init__.py +++ b/sso/services/mumble/__init__.py @@ -26,14 +26,14 @@ class MumbleService(BaseService): if tag: username = "[%s]%s" % (tag, username) + return self.raw_add_user(username, password, user) + + def raw_add_user(username, password): mumbleuser = MumbleUser() mumbleuser.name = username mumbleuser.password = password mumbleuser.server = self._get_server() - if 'user' in kwargs: - mumbleuser.user = kwargs['user'] - mumbleuser.save() return mumbleuser.name @@ -54,29 +54,17 @@ class MumbleService(BaseService): def disable_user(self, uid): """ Disable a user by uid """ - - srv = self._get_server() - try: - mumbleuser = MumbleUser.objects.get(name=uid, server=srv) - except MumbleUser.DoesNotExist: - return False - mumbleuser.password = "" - mumbleuser.save() - - for session in srv.players: - userdtl = srv.players[session] - if userdtl.name == uid: - srv.kickUser(session, "Account Disabled") + self.delete_user(uid) return True def enable_user(self, uid, password): """ Enable a user by uid """ - try: + if self.check_user(uid): mumbleuser = MumbleUser.objects.get(name=uid, server=self._get_server()) - except MumbleUser.DoesNotExist: - return False - mumbleuser.password = password - mumbleuser.save() + mumbleuser.password = password + mumbleuser.save() + else: + self.raw_add_user(uid, password) return True def reset_password(self, uid, password): From 34d5960110b46b7a61d194632e6176a61c4e5476 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Mon, 5 Apr 2010 23:25:08 +0100 Subject: [PATCH 096/116] Cleanup disconnection error message --- sso/services/jabber/xmppclient.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sso/services/jabber/xmppclient.py b/sso/services/jabber/xmppclient.py index 844b7f4..f432dc8 100644 --- a/sso/services/jabber/xmppclient.py +++ b/sso/services/jabber/xmppclient.py @@ -21,7 +21,8 @@ class JabberAdmin(): def __del__(self): - self._client.disconnect() + if hasattr(self, '_client'): + self._client.disconnect() def connect(self): if not hasattr(self, '_client'): From d05f73f99e342bcd3a1894c3f8459db490a4f6e1 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Tue, 6 Apr 2010 00:03:57 +0100 Subject: [PATCH 097/116] Small speedup for the update_access function --- sso/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sso/models.py b/sso/models.py index c6f3d88..1be665d 100644 --- a/sso/models.py +++ b/sso/models.py @@ -50,7 +50,7 @@ class SSOUser(models.Model): self._log.debug("Update - User %s" % self.user) # Create a list of all Corp groups corpgroups = [] - for corp in EVEPlayerCorporation.objects.all(): + for corp in EVEPlayerCorporation.objects.filter(group__isnull=False): if corp.group: corpgroups.append(corp.group) From 8af9b851e2c9af5c035641a02f066d8f0f00f248 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Tue, 6 Apr 2010 00:11:34 +0100 Subject: [PATCH 098/116] Small changes to the user lookup profile view --- templates/sso/lookup/user.html | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/templates/sso/lookup/user.html b/templates/sso/lookup/user.html index 1279ce3..8687a77 100644 --- a/templates/sso/lookup/user.html +++ b/templates/sso/lookup/user.html @@ -12,12 +12,11 @@

    Service Accounts

    {% if services %} - + {% for acc in services %} - - + {% endfor %}
    ServiceUsernamePasswordActive
    ServiceUsernameActive
    {{ acc.service }} {{ acc.service_uid }}******{{ acc.active }}{% if acc.active %}Yes{% else %}No{% endif %}
    From 5e4669955c00d2f2853946f1d958215def80331c Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Thu, 8 Apr 2010 08:24:53 +0100 Subject: [PATCH 099/116] Services fixes, Mumble deletion works and Wiki now returns values --- sso/services/mumble/__init__.py | 14 ++++++++++---- sso/services/wiki/__init__.py | 2 ++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/sso/services/mumble/__init__.py b/sso/services/mumble/__init__.py index 5f89e5a..7afe277 100644 --- a/sso/services/mumble/__init__.py +++ b/sso/services/mumble/__init__.py @@ -26,9 +26,9 @@ class MumbleService(BaseService): if tag: username = "[%s]%s" % (tag, username) - return self.raw_add_user(username, password, user) + return self.raw_add_user(username, password) - def raw_add_user(username, password): + def raw_add_user(self, username, password): mumbleuser = MumbleUser() mumbleuser.name = username mumbleuser.password = password @@ -48,8 +48,14 @@ class MumbleService(BaseService): def delete_user(self, uid): """ Delete a user by uid """ - mumbleuser = MumbleUser.objects.get(name=uid, server=self._get_server()) - mumbleuser.delete() + try: + mumbleuser = MumbleUser.objects.get(name=uid, server=self._get_server()) + except MumbleUser.DoesNotExist: + return True + try: + mumbleuser.delete() + except: + pass return True def disable_user(self, uid): diff --git a/sso/services/wiki/__init__.py b/sso/services/wiki/__init__.py index 8f58cb2..026a2fc 100644 --- a/sso/services/wiki/__init__.py +++ b/sso/services/wiki/__init__.py @@ -78,11 +78,13 @@ class MediawikiService(BaseService): self._dbcursor.execute(self.SQL_DEL_REV, [uid]) self._dbcursor.execute(self.SQL_DEL_USER, [uid]) self._db.connection.commit() + return True def disable_user(self, uid): """ Disable a user """ self._dbcursor.execute(self.SQL_DIS_USER, [uid]) self._db.connection.commit() + return True def enable_user(self, uid, password): """ Enable a user """ From 7b0017a3a295307959fb563e0bf312a2a5bae451 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Thu, 8 Apr 2010 08:57:50 +0100 Subject: [PATCH 100/116] Added a better disable function, now uses a Mediawiki group to destroy all access even if they can login --- sso/services/wiki/__init__.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/sso/services/wiki/__init__.py b/sso/services/wiki/__init__.py index 026a2fc..4f859fc 100644 --- a/sso/services/wiki/__init__.py +++ b/sso/services/wiki/__init__.py @@ -15,8 +15,10 @@ class MediawikiService(BaseService): 'provide_login': False } SQL_ADD_USER = r"INSERT INTO user (user_name, user_password, user_newpassword, user_email) VALUES (%s, %s, '', %s)" - SQL_DIS_USER = r"UPDATE user SET user_password = '', user_email = '' WHERE user_name = %s" + SQL_DIS_USER = r"UPDATE user SET user_password = '', user_email = '', user_token = %s WHERE user_name = %s" + SQL_DIS_GROUP = r"INSERT INTO user_groups (ug_user, ug_group) VALUES ((SELECT user_id FROM user WHERE user_name = %s), 'Disabled')" SQL_ENABLE_USER = r"UPDATE user SET user_password = %s WHERE user_name = %s" + SQL_ENABLE_GROUP = r"DELETE FROM user_groups where ug_user = (SELECT user_id FROM user WHERE user_name = %s) AND ug_group = 'Disabled'" SQL_CHECK_USER = r"SELECT user_name from user WHERE user_name = %s" SQL_DEL_REV = r"UPDATE revision SET rev_user = (SELECT user_id FROM user WHERE user_name = 'DeletedUser'), rev_user_text = 'DeletedUser' WHERE rev_user = (SELECT user_id FROM user WHERE user_name = %s)" @@ -50,6 +52,10 @@ class MediawikiService(BaseService): hash = hashlib.md5('%s-%s' % (salt, hashlib.md5(password).hexdigest())).hexdigest() return ":B:%s:%s" % (salt, hash) + def _gen_user_token(self): + hash = hashlib.md5(self._gen_salt()).hexdigest() + return hash + def _clean_username(self, username): username = username.strip() return username[0].upper() + username[1:] @@ -82,7 +88,8 @@ class MediawikiService(BaseService): def disable_user(self, uid): """ Disable a user """ - self._dbcursor.execute(self.SQL_DIS_USER, [uid]) + #self._dbcursor.execute(self.SQL_DIS_USER, [self._gen_user_token(), uid]) + self._dbcursor.execute(self.SQL_DIS_GROUP, [uid]) self._db.connection.commit() return True @@ -91,6 +98,8 @@ class MediawikiService(BaseService): pwhash = self._gen_mw_hash(password) self._dbcursor.execute(self.SQL_ENABLE_USER, [pwhash, uid]) self._db.connection.commit() + self._dbcursor.execute(self.SQL_ENABBLE_GROUP, [uid]) + self._db.connection.commit() return True def reset_password(self, uid, password): From 41fd4adfb61331c6492bca4ff2887b4654fdb24d Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Thu, 8 Apr 2010 09:23:49 +0100 Subject: [PATCH 101/116] Added catch for IntegrityError on disable user --- sso/services/wiki/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sso/services/wiki/__init__.py b/sso/services/wiki/__init__.py index 4f859fc..43918a9 100644 --- a/sso/services/wiki/__init__.py +++ b/sso/services/wiki/__init__.py @@ -1,6 +1,6 @@ import hashlib import random -from django.db import load_backend, transaction +from django.db import load_backend, transaction, IntegrityError from sso.services import BaseService import settings @@ -89,7 +89,11 @@ class MediawikiService(BaseService): def disable_user(self, uid): """ Disable a user """ #self._dbcursor.execute(self.SQL_DIS_USER, [self._gen_user_token(), uid]) - self._dbcursor.execute(self.SQL_DIS_GROUP, [uid]) + try: + self._dbcursor.execute(self.SQL_DIS_GROUP, [uid]) + except IntegrityError: + # Record already exists, skip it + pass self._db.connection.commit() return True From 33439366b9df000fca0227d514f358995e12dc65 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Thu, 8 Apr 2010 09:30:59 +0100 Subject: [PATCH 102/116] Added cron job to recheck users --- sso/cron.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/sso/cron.py b/sso/cron.py index 72a7e8e..9e7461e 100644 --- a/sso/cron.py +++ b/sso/cron.py @@ -23,4 +23,22 @@ class RemoveInvalidUsers(): # For each user, update access list based on Corp details user.get_profile().update_access() +class ValidateDisabledUsers(): + """ + Cycles through all users, check if their permissions are correct. + """ + # run daily + run_every = 84600 + + @property + def _logger(self): + if not hasattr(self, '__logger'): + self.__logger = logging.getLogger(__name__) + return self.__logger + + def job(self): + for servacc in ServiceAccount.objects.filter(active=0): + self.__logger.info('Checking %s' % servacc) + if not servacc.service.api_class.disable_user(servcc.service_uid): + self.__logger.error('Error disabling %s on %s" % (servacc, servacc.service)) From 1ecf5e49727286a5723adfba986956682ca7b898 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Thu, 8 Apr 2010 09:32:06 +0100 Subject: [PATCH 103/116] Fixed stupid typo --- sso/cron.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sso/cron.py b/sso/cron.py index 9e7461e..2986288 100644 --- a/sso/cron.py +++ b/sso/cron.py @@ -41,4 +41,4 @@ class ValidateDisabledUsers(): for servacc in ServiceAccount.objects.filter(active=0): self.__logger.info('Checking %s' % servacc) if not servacc.service.api_class.disable_user(servcc.service_uid): - self.__logger.error('Error disabling %s on %s" % (servacc, servacc.service)) + self.__logger.error('Error disabling %s on %s' % (servacc, servacc.service)) From 9850cac8ef9f3a58241aead81463c073c6950e13 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Thu, 8 Apr 2010 09:32:42 +0100 Subject: [PATCH 104/116] Referencing logger object before creation --- sso/cron.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sso/cron.py b/sso/cron.py index 2986288..490d0b1 100644 --- a/sso/cron.py +++ b/sso/cron.py @@ -39,6 +39,6 @@ class ValidateDisabledUsers(): def job(self): for servacc in ServiceAccount.objects.filter(active=0): - self.__logger.info('Checking %s' % servacc) + self._logger.info('Checking %s' % servacc) if not servacc.service.api_class.disable_user(servcc.service_uid): - self.__logger.error('Error disabling %s on %s' % (servacc, servacc.service)) + self._logger.error('Error disabling %s on %s' % (servacc, servacc.service)) From 0976089c17c30d290077f2d589a829820ba654b4 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Thu, 8 Apr 2010 09:33:39 +0100 Subject: [PATCH 105/116] Variable error fix --- sso/cron.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sso/cron.py b/sso/cron.py index 490d0b1..3127695 100644 --- a/sso/cron.py +++ b/sso/cron.py @@ -40,5 +40,5 @@ class ValidateDisabledUsers(): def job(self): for servacc in ServiceAccount.objects.filter(active=0): self._logger.info('Checking %s' % servacc) - if not servacc.service.api_class.disable_user(servcc.service_uid): + if not servacc.service.api_class.disable_user(seravcc.service_uid): self._logger.error('Error disabling %s on %s' % (servacc, servacc.service)) From 7d90b6d661020f71853bbd550b7f872bbfbb8a9e Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Thu, 8 Apr 2010 09:36:13 +0100 Subject: [PATCH 106/116] Variable issue --- sso/cron.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sso/cron.py b/sso/cron.py index 3127695..fd65585 100644 --- a/sso/cron.py +++ b/sso/cron.py @@ -40,5 +40,5 @@ class ValidateDisabledUsers(): def job(self): for servacc in ServiceAccount.objects.filter(active=0): self._logger.info('Checking %s' % servacc) - if not servacc.service.api_class.disable_user(seravcc.service_uid): + if not servacc.service.api_class.disable_user(servacc.service_uid): self._logger.error('Error disabling %s on %s' % (servacc, servacc.service)) From 81ffddae51f87b13cdb1cd85e1477a06846857ce Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Thu, 8 Apr 2010 09:52:45 +0100 Subject: [PATCH 107/116] Brought xmpp/auth.py up to speed for Python 2.6, stops depreciation warning for md5 --- sso/services/jabber/xmpp/auth.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/sso/services/jabber/xmpp/auth.py b/sso/services/jabber/xmpp/auth.py index 6e51d72..825b36c 100644 --- a/sso/services/jabber/xmpp/auth.py +++ b/sso/services/jabber/xmpp/auth.py @@ -23,9 +23,14 @@ from protocol import * from client import PlugIn import sha,base64,random,dispatcher,re -import md5 -def HH(some): return md5.new(some).hexdigest() -def H(some): return md5.new(some).digest() +try: + from hashlib import md5 +except ImportError: + import md5 as md5lib + def md5(val): return md5lib.new(val) + +def HH(some): return md5(some).hexdigest() +def H(some): return md5(some).digest() def C(some): return ':'.join(some) class NonSASL(PlugIn): From c5aa97da81aee94cab11b09b0eb6260aed2998e4 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Thu, 8 Apr 2010 09:58:06 +0100 Subject: [PATCH 108/116] Imported django-registration into repository, local changes needed --- registration/__init__.py | 0 registration/admin.py | 11 + registration/bin/delete_expired_users.py | 19 ++ registration/forms.py | 150 ++++++++++++ registration/locale/ar/LC_MESSAGES/django.mo | Bin 0 -> 2135 bytes registration/locale/ar/LC_MESSAGES/django.po | 81 ++++++ registration/locale/bg/LC_MESSAGES/django.mo | Bin 0 -> 2302 bytes registration/locale/bg/LC_MESSAGES/django.po | 78 ++++++ registration/locale/de/LC_MESSAGES/django.mo | Bin 0 -> 1909 bytes registration/locale/de/LC_MESSAGES/django.po | 85 +++++++ registration/locale/el/LC_MESSAGES/django.mo | Bin 0 -> 2424 bytes registration/locale/el/LC_MESSAGES/django.po | 84 +++++++ registration/locale/en/LC_MESSAGES/django.mo | Bin 0 -> 367 bytes registration/locale/en/LC_MESSAGES/django.po | 81 ++++++ registration/locale/es/LC_MESSAGES/django.mo | Bin 0 -> 1909 bytes registration/locale/es/LC_MESSAGES/django.po | 85 +++++++ .../locale/es_AR/LC_MESSAGES/django.mo | Bin 0 -> 1849 bytes .../locale/es_AR/LC_MESSAGES/django.po | 83 +++++++ registration/locale/fr/LC_MESSAGES/django.mo | Bin 0 -> 1883 bytes registration/locale/fr/LC_MESSAGES/django.po | 81 ++++++ registration/locale/he/LC_MESSAGES/django.mo | Bin 0 -> 1896 bytes registration/locale/he/LC_MESSAGES/django.po | 86 +++++++ registration/locale/it/LC_MESSAGES/django.mo | Bin 0 -> 1864 bytes registration/locale/it/LC_MESSAGES/django.po | 82 +++++++ registration/locale/ja/LC_MESSAGES/django.mo | Bin 0 -> 2035 bytes registration/locale/ja/LC_MESSAGES/django.po | 78 ++++++ registration/locale/nl/LC_MESSAGES/django.mo | Bin 0 -> 1898 bytes registration/locale/nl/LC_MESSAGES/django.po | 77 ++++++ registration/locale/pl/LC_MESSAGES/django.mo | Bin 0 -> 1769 bytes registration/locale/pl/LC_MESSAGES/django.po | 84 +++++++ .../locale/pt_BR/LC_MESSAGES/django.mo | Bin 0 -> 1796 bytes .../locale/pt_BR/LC_MESSAGES/django.po | 81 ++++++ registration/locale/ru/LC_MESSAGES/django.mo | Bin 0 -> 2360 bytes registration/locale/ru/LC_MESSAGES/django.po | 81 ++++++ registration/locale/sr/LC_MESSAGES/django.mo | Bin 0 -> 1966 bytes registration/locale/sr/LC_MESSAGES/django.po | 80 ++++++ registration/locale/sv/LC_MESSAGES/django.mo | Bin 0 -> 1687 bytes registration/locale/sv/LC_MESSAGES/django.po | 81 ++++++ .../locale/zh_CN/LC_MESSAGES/django.mo | Bin 0 -> 1669 bytes .../locale/zh_CN/LC_MESSAGES/django.po | 77 ++++++ .../locale/zh_TW/LC_MESSAGES/django.mo | Bin 0 -> 1669 bytes .../locale/zh_TW/LC_MESSAGES/django.po | 77 ++++++ registration/management/__init__.py | 0 registration/management/commands/__init__.py | 0 .../commands/cleanupregistration.py | 20 ++ registration/models.py | 231 ++++++++++++++++++ registration/urls.py | 60 +++++ registration/views.py | 164 +++++++++++++ 48 files changed, 2197 insertions(+) create mode 100644 registration/__init__.py create mode 100644 registration/admin.py create mode 100644 registration/bin/delete_expired_users.py create mode 100644 registration/forms.py create mode 100644 registration/locale/ar/LC_MESSAGES/django.mo create mode 100644 registration/locale/ar/LC_MESSAGES/django.po create mode 100644 registration/locale/bg/LC_MESSAGES/django.mo create mode 100644 registration/locale/bg/LC_MESSAGES/django.po create mode 100644 registration/locale/de/LC_MESSAGES/django.mo create mode 100644 registration/locale/de/LC_MESSAGES/django.po create mode 100644 registration/locale/el/LC_MESSAGES/django.mo create mode 100644 registration/locale/el/LC_MESSAGES/django.po create mode 100644 registration/locale/en/LC_MESSAGES/django.mo create mode 100644 registration/locale/en/LC_MESSAGES/django.po create mode 100644 registration/locale/es/LC_MESSAGES/django.mo create mode 100644 registration/locale/es/LC_MESSAGES/django.po create mode 100644 registration/locale/es_AR/LC_MESSAGES/django.mo create mode 100644 registration/locale/es_AR/LC_MESSAGES/django.po create mode 100644 registration/locale/fr/LC_MESSAGES/django.mo create mode 100644 registration/locale/fr/LC_MESSAGES/django.po create mode 100644 registration/locale/he/LC_MESSAGES/django.mo create mode 100644 registration/locale/he/LC_MESSAGES/django.po create mode 100644 registration/locale/it/LC_MESSAGES/django.mo create mode 100644 registration/locale/it/LC_MESSAGES/django.po create mode 100644 registration/locale/ja/LC_MESSAGES/django.mo create mode 100644 registration/locale/ja/LC_MESSAGES/django.po create mode 100644 registration/locale/nl/LC_MESSAGES/django.mo create mode 100644 registration/locale/nl/LC_MESSAGES/django.po create mode 100644 registration/locale/pl/LC_MESSAGES/django.mo create mode 100644 registration/locale/pl/LC_MESSAGES/django.po create mode 100644 registration/locale/pt_BR/LC_MESSAGES/django.mo create mode 100644 registration/locale/pt_BR/LC_MESSAGES/django.po create mode 100644 registration/locale/ru/LC_MESSAGES/django.mo create mode 100644 registration/locale/ru/LC_MESSAGES/django.po create mode 100644 registration/locale/sr/LC_MESSAGES/django.mo create mode 100644 registration/locale/sr/LC_MESSAGES/django.po create mode 100644 registration/locale/sv/LC_MESSAGES/django.mo create mode 100644 registration/locale/sv/LC_MESSAGES/django.po create mode 100644 registration/locale/zh_CN/LC_MESSAGES/django.mo create mode 100644 registration/locale/zh_CN/LC_MESSAGES/django.po create mode 100644 registration/locale/zh_TW/LC_MESSAGES/django.mo create mode 100644 registration/locale/zh_TW/LC_MESSAGES/django.po create mode 100644 registration/management/__init__.py create mode 100644 registration/management/commands/__init__.py create mode 100644 registration/management/commands/cleanupregistration.py create mode 100644 registration/models.py create mode 100644 registration/urls.py create mode 100644 registration/views.py diff --git a/registration/__init__.py b/registration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/registration/admin.py b/registration/admin.py new file mode 100644 index 0000000..3f36c18 --- /dev/null +++ b/registration/admin.py @@ -0,0 +1,11 @@ +from django.contrib import admin + +from registration.models import RegistrationProfile + + +class RegistrationAdmin(admin.ModelAdmin): + list_display = ('__unicode__', 'activation_key_expired') + search_fields = ('user__username', 'user__first_name') + + +admin.site.register(RegistrationProfile, RegistrationAdmin) diff --git a/registration/bin/delete_expired_users.py b/registration/bin/delete_expired_users.py new file mode 100644 index 0000000..87bd97f --- /dev/null +++ b/registration/bin/delete_expired_users.py @@ -0,0 +1,19 @@ +""" +A script which removes expired/inactive user accounts from the +database. + +This is intended to be run as a cron job; for example, to have it run +at midnight each Sunday, you could add lines like the following to +your crontab:: + + DJANGO_SETTINGS_MODULE=yoursite.settings + 0 0 * * sun python /path/to/registration/bin/delete_expired_users.py + +See the method ``delete_expired_users`` of the ``RegistrationManager`` +class in ``registration/models.py`` for further documentation. + +""" + +if __name__ == '__main__': + from registration.models import RegistrationProfile + RegistrationProfile.objects.delete_expired_users() diff --git a/registration/forms.py b/registration/forms.py new file mode 100644 index 0000000..7f10a4a --- /dev/null +++ b/registration/forms.py @@ -0,0 +1,150 @@ +""" +Forms and validation code for user registration. + +""" + + +from django import forms +from django.utils.translation import ugettext_lazy as _ +from django.contrib.auth.models import User + +from registration.models import RegistrationProfile + + +# I put this on all required fields, because it's easier to pick up +# on them with CSS or JavaScript if they have a class of "required" +# in the HTML. Your mileage may vary. If/when Django ticket #3515 +# lands in trunk, this will no longer be necessary. +attrs_dict = { 'class': 'required' } + + +class RegistrationForm(forms.Form): + """ + Form for registering a new user account. + + Validates that the requested username is not already in use, and + requires the password to be entered twice to catch typos. + + Subclasses should feel free to add any additional validation they + need, but should either preserve the base ``save()`` or implement + a ``save()`` which accepts the ``profile_callback`` keyword + argument and passes it through to + ``RegistrationProfile.objects.create_inactive_user()``. + + """ + username = forms.RegexField(regex=r'^\w+$', + max_length=30, + widget=forms.TextInput(attrs=attrs_dict), + label=_(u'username')) + email = forms.EmailField(widget=forms.TextInput(attrs=dict(attrs_dict, + maxlength=75)), + label=_(u'email address')) + password1 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict, render_value=False), + label=_(u'password')) + password2 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict, render_value=False), + label=_(u'password (again)')) + + def clean_username(self): + """ + Validate that the username is alphanumeric and is not already + in use. + + """ + try: + user = User.objects.get(username__iexact=self.cleaned_data['username']) + except User.DoesNotExist: + return self.cleaned_data['username'] + raise forms.ValidationError(_(u'This username is already taken. Please choose another.')) + + def clean(self): + """ + Verifiy that the values entered into the two password fields + match. Note that an error here will end up in + ``non_field_errors()`` because it doesn't apply to a single + field. + + """ + if 'password1' in self.cleaned_data and 'password2' in self.cleaned_data: + if self.cleaned_data['password1'] != self.cleaned_data['password2']: + raise forms.ValidationError(_(u'You must type the same password each time')) + return self.cleaned_data + + def save(self, profile_callback=None): + """ + Create the new ``User`` and ``RegistrationProfile``, and + returns the ``User``. + + This is essentially a light wrapper around + ``RegistrationProfile.objects.create_inactive_user()``, + feeding it the form data and a profile callback (see the + documentation on ``create_inactive_user()`` for details) if + supplied. + + """ + new_user = RegistrationProfile.objects.create_inactive_user(username=self.cleaned_data['username'], + password=self.cleaned_data['password1'], + email=self.cleaned_data['email'], + profile_callback=profile_callback) + return new_user + + +class RegistrationFormTermsOfService(RegistrationForm): + """ + Subclass of ``RegistrationForm`` which adds a required checkbox + for agreeing to a site's Terms of Service. + + """ + tos = forms.BooleanField(widget=forms.CheckboxInput(attrs=attrs_dict), + label=_(u'I have read and agree to the Terms of Service')) + + def clean_tos(self): + """ + Validate that the user accepted the Terms of Service. + + """ + if self.cleaned_data.get('tos', False): + return self.cleaned_data['tos'] + raise forms.ValidationError(_(u'You must agree to the terms to register')) + + +class RegistrationFormUniqueEmail(RegistrationForm): + """ + Subclass of ``RegistrationForm`` which enforces uniqueness of + email addresses. + + """ + def clean_email(self): + """ + Validate that the supplied email address is unique for the + site. + + """ + if User.objects.filter(email__iexact=self.cleaned_data['email']): + raise forms.ValidationError(_(u'This email address is already in use. Please supply a different email address.')) + return self.cleaned_data['email'] + + +class RegistrationFormNoFreeEmail(RegistrationForm): + """ + Subclass of ``RegistrationForm`` which disallows registration with + email addresses from popular free webmail services; moderately + useful for preventing automated spam registrations. + + To change the list of banned domains, subclass this form and + override the attribute ``bad_domains``. + + """ + bad_domains = ['aim.com', 'aol.com', 'email.com', 'gmail.com', + 'googlemail.com', 'hotmail.com', 'hushmail.com', + 'msn.com', 'mail.ru', 'mailinator.com', 'live.com'] + + def clean_email(self): + """ + Check the supplied email address against a list of known free + webmail domains. + + """ + email_domain = self.cleaned_data['email'].split('@')[1] + if email_domain in self.bad_domains: + raise forms.ValidationError(_(u'Registration using free email addresses is prohibited. Please supply a different email address.')) + return self.cleaned_data['email'] diff --git a/registration/locale/ar/LC_MESSAGES/django.mo b/registration/locale/ar/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..07bc79f1242872c701e463e947846ef4e77f8ceb GIT binary patch literal 2135 zcmb`GO>Y}j6oxO*QffYhKrCQ!#Dcy6I+Tvl^ALK%-lJj?>YYCZ?1GEo`4N|6c3G!#`-*whp((Yy+m z-Kr90Rd=-srEvqF+S>K&w8ZwRDP50btrjY+l_pnHD-0U$E!U`8n&vzuwIbbawY(LQ zRCAY>RH%G2>X;Ul20TZCaVY6=c`M{{PW8Cq;1c?>sfHsNd0Y8MfvSxlKzY&+&~%s< z3w;kwRq0dUqvmSh8;Sg$GDd~^BKhs+Ej+}Uwf!0%s{xu9H-k1c+uDo@2_FavRl`_7 z6&~A}l@|1d*qUo>Nv*#O!Wt=AZIE%BO312lmlMTrs}(V#ju_YN1^9MlBqu` z3}eal)QN{$utM<<%OvK);0skXPNC*3qAg51OLMuYPjWZ%w3si?7iMQtWz`Bo)NRVSxU{#R-7r!ir2%LmPTdiQYQ1hlbLYFCTMIT`)+OY?*SF{c@gpRgwgbFt8Js8F>X~IztA(6go@3S{MAyi!7zXaki z`?ib9SJPtHfr}r`&fN@Gz>eI+M)JVgbpEe0pa?4w3p@}92mg_=6F0I0Ju$062%PoO ziTdj-=q`RB&d+EWA|2cDEQeAJX4!#dFIu(VvSp8Z~sGVt;dyNyAm~cwR)@sXJ^ro7AAbs!, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-19 19:30-0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: forms.py:38 +msgid "username" +msgstr "ь╖ьЁы┘ ь╖ы└ы┘ьЁь╙ь╝ь╞ы┘" + +#: forms.py:41 +msgid "email address" +msgstr "ь╧ы├ы┬ь╖ы├ ь╖ы└ь╗ь╠ы┼ь╞ ь╖ы└ь╖ы└ы┐ь╙ь╠ы┬ы├ы┼" + +#: forms.py:43 +msgid "password" +msgstr "ы┐ы└ы┘ь╘ ь╖ы└ы┘ь╠ы┬ь╠" + +#: forms.py:45 +msgid "password (again)" +msgstr "ь╙ьёы┐ы┼ь╞ ы┐ы└ы┘ь╘ ь╖ы└ы┘ь╠ы┬ь╠" + +#: forms.py:54 +msgid "Usernames can only contain letters, numbers and underscores" +msgstr "ы┼ы┘ы┐ы├ ьёы├ ы┼ь╜ь╙ы┬ы┼ ь╖ьЁы┘ ь╖ы└ы┘ьЁь╙ь╝ь╞ы┘ ь╧ы└ы┴ ь╖ь╜ь╠ы│ь▄ ь╖ь╠ы┌ь╖ы┘ ы┬ь╢ь╠ь╥ь╖ь╙ ьЁь╥ь╠ы┼ь╘ ы│ы┌ь╥" + +#: forms.py:59 +msgid "This username is already taken. Please choose another." +msgstr "ь╖ьЁы┘ ь╖ы└ы┘ьЁь╙ь╝ь╞ы┘ ы┘ьЁь╛ы└ ы┘ьЁь╗ы┌ь╖. ы┼ь╠ь╛ы┴ ь╖ь╝ь╙ы┼ь╖ь╠ ь╖ьЁы┘ ь╖ь╝ь╠." + +#: forms.py:68 +msgid "You must type the same password each time" +msgstr "ы┼ь╛ь╗ ь╖ь╞ь╝ь╖ы└ ы┐ы└ы┘ь╘ ь╖ы└ы┘ь╠ы┬ь╠ ы┘ь╥ь╖ь╗ы┌ь╘ ы┐ы└ ы┘ь╠ь╘" + +#: forms.py:96 +msgid "I have read and agree to the Terms of Service" +msgstr "ьёы┌ь╠ ь╗ы┌ь╠ь╖ь║ь╘ ы┬ь╖ы└ы┘ы┬ь╖ы│ы┌ь╘ ь╧ы└ы┴ ь╢ь╠ы┬ь╥ ь╖ы└ь╝ь╞ы┘ь╘" + +#: forms.py:105 +msgid "You must agree to the terms to register" +msgstr "ы┼ь╛ь╗ ь╖ы└ы┘ы┬ь╖ы│ы┌ь╘ ь╧ы└ы┴ ь╖ы└ь╢ь╠ы┬ь╥ ы└ы└ь╙ьЁь╛ы┼ы└" + +#: forms.py:124 +msgid "" +"This email address is already in use. Please supply a different email " +"address." +msgstr "ь╧ы├ы┬ь╖ы├ ь╖ы└ь╗ь╠ы┼ь╞ ь╖ы└ь╖ы└ы┐ь╙ь╠ы┬ы├ы┼ ы┘ьЁь╛ы└ ы┘ьЁь╗ы┌ь╖. ы┼ь╠ь╛ы┴ ь╙ь╡ы┬ы┼ь╞ ь╧ы├ы┬ь╖ы├ ь╗ь╠ы┼ь╞ ь╖ы└ы┐ь╙ь╠ы┬ы├ы┼ ы┘ь╝ь╙ы└ы│." + +#: forms.py:149 +msgid "" +"Registration using free email addresses is prohibited. Please supply a " +"different email address." +msgstr "ы┼ы┘ы├ь╧ ь╖ы└ь╙ьЁь╛ы┼ы└ ь╗ь╖ьЁь╙ь╝ь╞ь╖ы┘ ь╧ы├ь╖ы┬ы┼ы├ ь╗ь╠ы┼ь╞ ь╖ы└ы┐ь╙ь╠ы┬ы├ы┼ь╘ ы┘ь╛ь╖ы├ы┼ь╘. ы┼ь╠ь╛ы┴ ь╙ь╡ы┬ы┼ь╞ ь╧ы├ы┬ь╖ы├ ь╗ь╠ы┼ь╞ ь╖ы└ы┐ь╙ь╠ы┬ы├ы┼ ы┘ь╝ь╙ы└ы│." + +#: models.py:188 +msgid "user" +msgstr "ы┘ьЁь╙ь╝ь╞ы┘" + +#: models.py:189 +msgid "activation key" +msgstr "ь╠ы┘ь╡ ь╖ы└ь╙ы│ь╧ы┼ы└" + +#: models.py:194 +msgid "registration profile" +msgstr "ы┘ы└ы│ ь╖ы└ь╙ьЁь╛ы┼ы└ ь╖ы└ь╢ь╝ь╣ы┼" + +#: models.py:195 +msgid "registration profiles" +msgstr "ы┘ы└ы│ь╖ь╙ ь╖ы└ь╙ьЁь╛ы┼ы└ ь╖ы└ь╢ь╝ь╣ы┼ь╘" diff --git a/registration/locale/bg/LC_MESSAGES/django.mo b/registration/locale/bg/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..be9adf4d3defce5ee8274e2cb996d8c63100a637 GIT binary patch literal 2302 zcmb7D&u<$=6doW@Fi;QzaR9^<5~875V>f~(ZmF7vP(`Gvl{yh{sV4TsUSjWByJOP` zA(6DAr3iwAdZ{XbxNzX&{4lZNB)uWH&7P6C@E7nG!1reTBZ0IME3dzqH*eni-uK@A z@yx(2f#+$wFXFv}_jz7FfImD}fDUj8cp3O3a2j~zK_T7(&H+yWzXHAn{0%q={0n## z_`*X%JPCXq_$Y7$$hpRV&jM$F&jE|T0pO=V_FDml$n_;~9{ay>_!DscVQ>K+eME>4 zfj5Ak0RIG*fR~TNe0~KI_5%p^67W^v6Torc%fKv)zd)B%POgAO_ox=SUXd2qUsa@ACRr#1DhyRfZb;R@FS+xsR)r*G z%Swid!dkUjULr{acVR&V%F_ogCPl7F<(e>{w z=$I^m=%9!*by|gHEVyNLs4*0*ko{wsL^kk0QhDu67MwYhg-&NEJ2w8_*!c;Xo0z#Y zd2u>1qpE(OoeN>nEjZ_D#n8$58A@a?=A3c#ipRrwM{1R!6RFfmCpGG%M=3p;8BRH= zH&ZD%Iy0*7a`3+2h?5$|F{MvrhEE<(;bvk=hT6#m(hJK{`$2~0%Cg{MQ>WAQ?lM_% zwO>D5(==_0mZO_yhfJ%xvW5-QHjQW*9=otp zbQSBi*)!{=6|F`qra@+l|C(kSCwJ{xgYD7h>>QV38zq2l?C4Dw%aKUWaiL?lvNOZGl6}*0j4XSdV5K z{@a$z9s=$%_q)*G3)2whn_g;I?Gc3ew?7xxyJH!)Yzg25TxXv5*S$l>O^8Fs#qL1Y4YX*5 zplBl=gU#qB!u}jv*Fb!mTR%j$z1MJrCgBGOQ5XgZ#X%qVY@3!aKVZ8GYEY#YGj2UP z*BveH`u#>L=E4KR`t)b1g*{JA6GeW@XVDF0<=(`!7=lZ!h414IQ&Y_AwX~$h^OT_-o)f, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-19 19:30-0500\n" +"PO-Revision-Date: 2008-03-05 12:37+0200\n" +"Last-Translator: Vladislav \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Poedit-Bookmarks: -1,-1,-1,-1,10,-1,-1,-1,-1,-1\n" + +#: forms.py:38 +msgid "username" +msgstr "п÷п╬я┌я─п╣п╠п╦я┌п╣п╩я│п╨п╬ п╦п╪п╣ " + +#: forms.py:41 +msgid "email address" +msgstr "п∙п╩п╣п╨я┌я─п╬п╫п╫п╟ п©п╬я┴п╟" + +#: forms.py:43 +msgid "password" +msgstr "п÷п╟я─п╬п╩п╟" + +#: forms.py:45 +msgid "password (again)" +msgstr "п÷п╟я─п╬п╩п╟ (п©я─п╬п╡п╣я─п╨п╟)" + +#: forms.py:54 +msgid "Usernames can only contain letters, numbers and underscores" +msgstr "п÷п╬я┌я─п╣п╠п╦я┌п╣п╩я│п╨п╦я┌п╣ п╦п╪п╣п╫п╟ п╪п╬пЁп╟я┌ п╢п╟ я│я┼п╢я┼я─п╤п╟я┌ п╠я┐п╨п╡п╦, я├п╦я└я─п╦ п╦ п©п╬п╢я┤п╣я─я┌п╟п╡п╨п╦" + +#: forms.py:59 +msgid "This username is already taken. Please choose another." +msgstr "п÷п╬я┌я─п╣п╠п╦я┌п╣п╩я│п╨п╬я┌п╬ п╦п╪п╣ п╣ п╥п╟п╣я┌п╬. п°п╬п╩я▐ п╦п╥п╠п╣я─п╣я┌п╬ п╢я─я┐пЁп╬." + +#: forms.py:68 +msgid "You must type the same password each time" +msgstr "п⌠я─п╣я┬п╨п╟ п©я─п╦ п©я─п╬п╡п╣я─п╨п╟ п╫п╟ п©п╟я─п╬п╩п╟я┌п╟." + +#: forms.py:96 +msgid "I have read and agree to the Terms of Service" +msgstr "п÷я─п╬я┤п╣п╩ я│я┼п╪ п╦ я│я┼п╪ я│я┼пЁп╩п╟я│п╣п╫ я│ я┐я│п╩п╬п╡п╦я▐я┌п╟ п╥п╟ п╣п╨я│п©п╩п╬п╟я┌п╟я├п╦я▐" + +#: forms.py:105 +msgid "You must agree to the terms to register" +msgstr "п╒я─я▐п╠п╡п╟ п╢п╟ я│я┌п╣ я│я┼пЁп╩п╟я│п╫п╦ я│ я┐я│п╩п╬п╡п╦я▐я┌п╟ п╥п╟ п╢п╟ я│п╣ я─п╣пЁп╦я│я┌я─п╦я─п╟я┌п╣." + +#: forms.py:124 +msgid "This email address is already in use. Please supply a different email address." +msgstr "п░п╢я─п╣я│п╟ п╫п╟ п╣п╩п╣п╨я┌я─п╬п╫п╫п╟я┌п╟ п©п╬я┴п╟ п╣ п╦п╥п©п╬п╩п╥п╡п╟п╫. п°п╬п╩я▐ п╡я┼п╡п╣п╢п╣я┌п╣ п╢я─я┐пЁ п╟п╢я─п╣я│." + +#: forms.py:149 +msgid "Registration using free email addresses is prohibited. Please supply a different email address." +msgstr "п═п╣пЁп╦я│я┌я─п╟я├п╦п╦я┌п╣ я│ п╠п╣п╥п©п╩п╟я┌п╫п╦ п╟п╢я─п╣я│п╦ п╣ п╥п╟п╠я─п╟п╫п╣п╫. п°п╬п╩я▐ п╡я┼п╡п╣п╢п╣я┌п╣ я─п╟п╥п╩п╦я┤п╣п╫ п╟п╢я─п╣я│ п╥п╟ п╣п╩п╣п╨я┌я─п╬п╫п╫п╟ п©п╬я┴п╟" + +#: models.py:188 +msgid "user" +msgstr "п÷п╬я┌я─п╣п╠п╦я┌п╣п╩" + +#: models.py:189 +msgid "activation key" +msgstr "п п╩я▌я┤ п╥п╟ п╟п╨я┌п╦п╡п╟я├п╦я▐" + +#: models.py:194 +msgid "registration profile" +msgstr "я─п╣пЁп╦я│я┌я─п╟я├п╦п╬п╫п╣п╫ п©я─п╬я└п╦п╩" + +#: models.py:195 +msgid "registration profiles" +msgstr "я─п╣пЁп╦я│я┌я─п╟я├п╦п╬п╫п╫п╦ п©я─п╬я└п╦п╩п╦" + diff --git a/registration/locale/de/LC_MESSAGES/django.mo b/registration/locale/de/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..b9bc1faa0953a658be81c2df89acaef26762cb74 GIT binary patch literal 1909 zcmbVM&u`pB6ds_Zut2F$fde4D8x7Q3r==y?{!o)9MT?p?N;gGtsb=lB_H^x;)y#O) zjl>0xh$FWik+^c=)Z3o8az)}#K;l2(8+&)tjdB4?_BUg{AK!fMJ^%I2sh_zUnp@XX6XTmwD=UI+dN{22H zvHunL9`J8q;Bop)6ZbqYy#ELo@_z;l`Mb04kAZJup97J;z5@U7F7SK1e$za{Io_D< zkw$=OeFgsW3t0?G`K@ldDFfVnUKqH5jmE?Ux;NsA~hKe&WX`p zB7ec&vvn6qS7jfITC+;0SSALC7hju-%F6j=A>jidp{lI~WP4=iCnM+$v0dOelFl6% zn-a?;C$CBtGVyBMC_dzgSW-vK?H3EWATtnM5lyC9sZfl8D)_{gP6UPON03SETJwYx zA9d5{5srmQcPM=#b!MWam~s0Gwf1;qtdDkFrqXD$%3Rbl9ct}9=tXx>w9qHfmh{}A z%W?d96t73C>$JMwxe`b5)i{P>w8vu=vK+I!47+QctMR8XJX`mq^HI-A?F#9Q?a-G} zYqZ8aRw);B1N^YPUDVsQ(gSk~K02#p#!-)D*`Y12yo0DWQodcNwz1i*)=qb4`{XboN85?!Rq zWuMXPO!q5)Sn14-(e+CArNxFaF4!@88e`{B>d>OOrNc^W!DTjTifh|aVOEr8OGD#8 zut39V+KzTGchN>Oc4%;qV4*){=G(NX(5uMOhvh>=4E~{C!Qr^4)a{TILvSqQfkH1r zHH>@48mIW5aV>4u?u$|Mv#3y{W;mxCNa`6&O$XDT^8%A9Ui8*9oc^YRnXVAGN^<8R zw7y8+$Y5Oyx?k(ytx7OjC}=JV$R>gg<@7mzQko#d$%Q$@{DyKMVpWnu2oz;Dx)udN zoJQMJI?B*9JjMV3 literal 0 HcmV?d00001 diff --git a/registration/locale/de/LC_MESSAGES/django.po b/registration/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..96e00ee --- /dev/null +++ b/registration/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,85 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Jannis Leidel , 2007. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: django-registration 0.3 \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-19 19:30-0500\n" +"PO-Revision-Date: 2007-09-29 16:50+0200\n" +"Last-Translator: Jannis Leidel \n" +"Language-Team: Deutsch \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: forms.py:38 +msgid "username" +msgstr "Benutzername" + +#: forms.py:41 +msgid "email address" +msgstr "E-Mail-Adresse" + +#: forms.py:43 +msgid "password" +msgstr "Passwort" + +#: forms.py:45 +msgid "password (again)" +msgstr "Passwort (wiederholen)" + +#: forms.py:54 +msgid "Usernames can only contain letters, numbers and underscores" +msgstr "Benutzernamen kц╤nnen nur Buchstaben, Zahlen und Unterstriche enthalten" + +#: forms.py:59 +msgid "This username is already taken. Please choose another." +msgstr "Dieser Benutzername ist schon vergeben. Bitte einen anderen wц╓hlen." + +#: forms.py:68 +msgid "You must type the same password each time" +msgstr "Bitte das gleiche Passwort zur ц°berprц╪fung nochmal eingeben" + +#: forms.py:96 +msgid "I have read and agree to the Terms of Service" +msgstr "Ich habe die Nutzungsvereinbarung gelesen und stimme ihr zu" + +#: forms.py:105 +msgid "You must agree to the terms to register" +msgstr "Sie mц╪ssen der Nutzungsvereinbarung zustimmen, um sich zu registrieren" + +#: forms.py:124 +msgid "" +"This email address is already in use. Please supply a different email " +"address." +msgstr "" +"Diese E-Mail-Adresse wird schon genutzt. Bitte geben Sie eine andere " +"E-Mail-Adresse an." + +#: forms.py:149 +msgid "" +"Registration using free email addresses is prohibited. Please supply a " +"different email address." +msgstr "" +"Die Registrierung mit einer kostenlosen E-Mail-Adresse ist untersagt. Bitte " +"geben Sie eine andere E-Mail-Adresse an." + +#: models.py:188 +msgid "user" +msgstr "Benutzer" + +#: models.py:189 +msgid "activation key" +msgstr "Aktivierungsschlц╪ssel" + +#: models.py:194 +msgid "registration profile" +msgstr "Registrierungsprofil" + +#: models.py:195 +msgid "registration profiles" +msgstr "Registrierungsprofile" diff --git a/registration/locale/el/LC_MESSAGES/django.mo b/registration/locale/el/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..acc972683e0c03dee9cf816301d9653f1f8bccc3 GIT binary patch literal 2424 zcmbtUO>Y}j6n#LUU_Mn55CWvjiner&-4aOLQq?4G72%{Iae}&n7yHE?YR_0Rr3E=*#C^fI`E}Oh1drE3j7@S z^Ay7hpLKr003n)neQLJQGD1v#e3Q!+==!JGG#qQIq~ zpeUofl27g^4JvQU&MGmW^0ps%GO%5TD!%RHX_Wm{N!mq{xtynbU-@MFRQBA0Jz@tc z7p4B9lD;CpQZ5(ANm9-p9aWxkg2NZ1B2&Q4L%}$dEHb@uvN@-E&~UioIkKbm(g!D+h>kt>HD9$UXGN^7AUYwO2}+rk2N}eU5$%F z-4XZvTMIfV^B{UkG%__ug<*`^MRlan7u+H9!)+4%p8JW)23Akb8b(=|bb|VmUGFEa zq-Z!bFxYdoFEXIYt`}Ine%{Vm?^W`?m2ndk>A#w>x-cu2ht(|um7ue+*gICN(~5Ue zyfbkwX2sfLF*sTSYRu-~2mRu4EB+RpjVIb;Z^Upjl9qm8WjyKlMH#qWf-VoG)6|#j zP0@u^Z?Y$SDcRjUkQyAk2v;Xxk$Gihlq@AEo$R|Z#FQ_j)0c{N)b;WgBfUMnsr{nj z(O9GlbA=AA45pBvK-~!5EEmzg^OP+}&sV{_Lz&A~$G#tPfp)EwlXY{@e}Xz7@`%vi z=q+8ovVa;dD5wcf+}GUf07V(z|+-!Wq36&W2N@7sAQr61Ey!eheqD znADpHvWiesLf<64sn_*7tZU(%Ug03ih&xYuBb;Ynq^)Cv{F}&jCtO6XX{?cxbWQKT zV>_JTT)GaYZRA{J7Hi=b@ZT^8AclnCMKDi*`#LtvV-YW0^;VShEu4XcscnbL*$9`w zWClgSR>i$N#7FT}B;C=qD6=;$SjRcDn8Z2ggGcQi<3%u@HH=|;V74e>5*?d1arc@G zP)dU{=@s;)_TQ*~g(1y^3uayvE`akMxUZ2(xnk;p-79&Gyq~(CEY~Z!T!`R$l`<%-gBm1RRIfs7F)Qt z%HLPrG>tXS8eTPu!UrjuU)BH8s4!o^KUm$zOeghSOs=l0r$jhmW;+K$JAeBZ0Jr;| PvNps)m9q2E`W1fx4LxjP literal 0 HcmV?d00001 diff --git a/registration/locale/el/LC_MESSAGES/django.po b/registration/locale/el/LC_MESSAGES/django.po new file mode 100644 index 0000000..cd38eb1 --- /dev/null +++ b/registration/locale/el/LC_MESSAGES/django.po @@ -0,0 +1,84 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Panos Laganakos , 2007. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-19 19:30-0500\n" +"PO-Revision-Date: 2007-11-14 21:50+0200\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: forms.py:38 +msgid "username" +msgstr "о▄н╫н©н╪н╠ о┤о│н╝о┐о└н╥" + +#: forms.py:41 +msgid "email address" +msgstr "н╢н╧н╣о█н╦о┘н╫о┐н╥ н╥н╩н╣н╨о└о│н©н╫н╧н╨н©о█ о└н╠о┤о┘н╢о│н©н╪н╣н╞н©о┘" + +#: forms.py:43 +msgid "password" +msgstr "о┐о┘н╫н╦н╥н╪н╠о└н╧н╨о▄" + +#: forms.py:45 +msgid "password (again)" +msgstr "о┐о┘н╫н╦н╥н╪н╠о└н╧н╨о▄ (н╬н╠н╫н╛)" + +#: forms.py:54 +msgid "Usernames can only contain letters, numbers and underscores" +msgstr "н╓н╠ н©н╫о▄н╪н╠о└н╠ о┤о│н╥о┐о└о▌н╫ н╪о─н©о│н©о█н╫ н╫н╠ о─н╣о│н╧н╩н╠н╪н╡н╛н╫н©о┘н╫ н╪о▄н╫н© нЁо│н╛н╪н╪н╠о└н╠, н╠о│н╧н╦н╪н©о█о┌ н╨н╠н╧ о┘о─н©нЁо│н╠н╪н╪н╞о┐н╣н╧о┌" + +#: forms.py:59 +msgid "This username is already taken. Please choose another." +msgstr "н▒о┘о└о▄ о└н© о▄н╫н©н╪н╠ о┤о│н╝о┐о└н╥ о┤о│н╥о┐н╧н╪н©о─н©н╞н╣н╧о└н╠н╧ н╝н╢н╥. н═н╠о│н╠н╨н╠н╩о▌ н╢н╧н╠н╩н╜н╬о└н╣ н╜н╫н╠ н╛н╩н╩н©." + +#: forms.py:68 +msgid "You must type the same password each time" +msgstr "н═о│н╜о─н╣н╧ н╫н╠ н╣н╧о┐н╛нЁн╣о└н╣ о└н© н╞н╢н╧н© о┐о┘н╫н╦н╥н╪н╠о└н╧н╨о▄ н╨н╛н╦н╣ о├н©о│н╛" + +#: forms.py:96 +msgid "I have read and agree to the Terms of Service" +msgstr "н■н╧н╛н╡н╠о┐н╠ н╨н╠н╧ о┐о┘н╪о├о┴н╫о▌ н╪н╣ о└н©о┘о┌ н▄о│н©о┘о┌ о└н╥о┌ н╔о─н╥о│н╣о┐н╞н╠о┌" + +#: forms.py:105 +msgid "You must agree to the terms to register" +msgstr "н═о│н╜о─н╣н╧ н╫н╠ о┐о┘н╪о├о┴н╫н╣н╞о└н╠н╧ н╪н╣ о└н©о┘о┌ о▄о│н©о┘о┌ нЁн╧н╠ н╫н╠ н╣нЁнЁо│н╠о├н╣н╞о└н╣" + +#: forms.py:124 +msgid "" +"This email address is already in use. Please supply a different email " +"address." +msgstr "" +"н≈ о┐о┘нЁн╨н╣н╨о│н╧н╪н╜н╫н╥ н╢н╧н╣о█н╦о┘н╫о┐н╥ н╥н╩н╣н╨о└о│н©н╫н╧н╨н©о█ о└н╠о┤о┘н╢о│н©н╪н╣н╞н©о┘ о┤о│н╥о┐н╧н╪н©о─н©н╧н╣н╞о└н╠н╧ н╝н╢н╥. " +"н═н╠о│н╠н╨н╠н╩о▌ н╢о▌о┐о└н╣ н╨н╛о─н©н╧н╠ н╛н╩н╩н╥." + +#: forms.py:149 +msgid "" +"Registration using free email addresses is prohibited. Please supply a " +"different email address." +msgstr "" +"н≈ н╣нЁнЁо│н╠о├н╝ н╪н╜о┐о┴ н╢о┴о│н╣н╛н╫ н╢н╧н╣о┘н╦о█н╫о┐н╣о┴н╫ н╥н╩н╣н╨о└о│н©н╫н╧н╨н©о█ о└н╠о┤о┘н╢о│н©н╪н╣н╞н©о┘ н╠о─н╠нЁн©о│н╣о█н╣о└н╠н╧. ""н═н╠о│н╠н╨н╠н╩о▌ н╢о▌о┐о└н╣ н╨н╛о─н©н╧н╠ н╛н╩н╩н╥." + +#: models.py:188 +msgid "user" +msgstr "о┤о│н╝о┐о└н╥о┌" + +#: models.py:189 +msgid "activation key" +msgstr "н╨н╩н╣н╧н╢н╞ н╣н╫н╣о│нЁн©о─н©н╞н╥о┐н╥о┌" + +#: models.py:194 +msgid "registration profile" +msgstr "о─о│н©о├н╞н╩ н╣нЁнЁо│н╠о├н╝о┌" + +#: models.py:195 +msgid "registration profiles" +msgstr "о─о│н©о├н╞н╩ н╣нЁнЁо│н╠о├о▌н╫" diff --git a/registration/locale/en/LC_MESSAGES/django.mo b/registration/locale/en/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..4728fe062f9baac03c648bc2102e16292b50f838 GIT binary patch literal 367 zcmYL^K~KUk7=|%=+R?Lz9=z#?MMGp236&LGY&S9niQX#IS%dA;6{A1IzvpkUON_k9 zlOFnf?fd;4AANU14gx2E)4*ZiJkVwk=!eTVoUQYpxwCI-?IGR3O1VZ`L(rT~9_Wmr z6)P5Lo<)VKt@9w7N^jt9S2a*tf}(j|!o(@*!w@9WD}pXDz6KmaFGjtXTw%a{jDrY; z`q1s;f8Hab1&ACHitckF(zB;LV-c5)htf&YY^Ar3py`rxlu^OZkO`XdF-?+!Ef%Ao znrbI21*MYj1aX?pmTl!B=i{yJT33xCkqWk7s@KKQ#2T+m_~WY%Wxe|J7xh+ZbA#5e e(lyH8F3Twl_FmiNMLA$*Z8zFf1Pz|Gk2=2%fMUP^ literal 0 HcmV?d00001 diff --git a/registration/locale/en/LC_MESSAGES/django.po b/registration/locale/en/LC_MESSAGES/django.po new file mode 100644 index 0000000..37bc9e2 --- /dev/null +++ b/registration/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,81 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-19 19:30-0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: forms.py:38 +msgid "username" +msgstr "" + +#: forms.py:41 +msgid "email address" +msgstr "" + +#: forms.py:43 +msgid "password" +msgstr "" + +#: forms.py:45 +msgid "password (again)" +msgstr "" + +#: forms.py:54 +msgid "Usernames can only contain letters, numbers and underscores" +msgstr "" + +#: forms.py:59 +msgid "This username is already taken. Please choose another." +msgstr "" + +#: forms.py:68 +msgid "You must type the same password each time" +msgstr "" + +#: forms.py:96 +msgid "I have read and agree to the Terms of Service" +msgstr "" + +#: forms.py:105 +msgid "You must agree to the terms to register" +msgstr "" + +#: forms.py:124 +msgid "" +"This email address is already in use. Please supply a different email " +"address." +msgstr "" + +#: forms.py:149 +msgid "" +"Registration using free email addresses is prohibited. Please supply a " +"different email address." +msgstr "" + +#: models.py:188 +msgid "user" +msgstr "" + +#: models.py:189 +msgid "activation key" +msgstr "" + +#: models.py:194 +msgid "registration profile" +msgstr "" + +#: models.py:195 +msgid "registration profiles" +msgstr "" diff --git a/registration/locale/es/LC_MESSAGES/django.mo b/registration/locale/es/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..3872adf0a0eed30b1ec9fa6410addb3c6db915cf GIT binary patch literal 1909 zcmb7^zmFU>6vqt&0tegqGU+h77t!JFWZ;78!eM}>GBybfLfzX4wZe+4(dzrkn0R~{4M6!<#$ zB)A1eU+;l0fCu1F{1NXF zZN>rf0-<&z#=jiVCZrGuv(uvoK7}cEXe=j8j%7j8V3v-Vf~7EK%GuSP?1-+in`p)2 zfJ^OzlR;ZUt=FccQQXh9)D_91aO^#M(w-V;$NEqQE>haBSb8SkHcd4pNrfJb*s%%g z2UC%cadORAG$pGT-jsCo$^X|JjyNN0UbPhDEjITIsj;;PPa2C$yHs3TobaTiA!`t` zvL;CQuQ&vD{ydqsKE$HatThFe%Hs0k6WdbV`min}&Ok`0>N)}09q$X%26{tmA7gB! z_n%o;5KA?tplcRV1wEN7ev7AKO&zh^e{VtOWQjmG#N5*&RY=B2SNyLZy@(2Rk0_Ja zclL-?NOp?kIT=Udyi}TgUrigedJ~Xp}9`9s}A!5A?)` zjMAC?Tv$T=D{c^ZWl<5cpIogCRs{;6Ycg%cuv*lKvnY6UC17ILI95u}?wYt8<>uHD zSICvax2QuI(~So5VfOv90SlY%TSp@~!4q|Wd?~0c$)b-}Y^Gv&_4U78Oy$D7*W3l> zWg*c?OBWCIF!X`R*!s|MwBPzxIt@vdHmzzKiGYvI!I`op$6&44g--L+onDLMp8V`? zWvOYoVAFLf8+Nf(os6A2`=w@wqf;td+#Ekohw{jJk>dt<`Lspr$COB%S<6Wyq0qAC zUEtLd+90B$RT_sR)!Nsxa}Ad~yMr}kp$UI3R26?;aoc$xcli?!r1HK@iuil8Ftxg- fb6wV8cSAJn(9_ZKB1F6RggdQ%T*xFI&|UlkAoEAZ literal 0 HcmV?d00001 diff --git a/registration/locale/es/LC_MESSAGES/django.po b/registration/locale/es/LC_MESSAGES/django.po new file mode 100644 index 0000000..ba0384f --- /dev/null +++ b/registration/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,85 @@ +# Spanish translation for django-registration. +# Copyright (C) 2007, James Bennet +# This file is distributed under the same license as the registration package. +# Ernesto Rico Schmidt , 2008. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: django-registration 0.3 \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2008-03-11 00:19-0400\n" +"PO-Revision-Date: 2008-03-11 00:19-0400\n" +"Last-Translator: Ernesto Rico Schmidt \n" +"Language-Team: Espaц╠ol \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: forms.py:38 +msgid "username" +msgstr "nombre de usuario" + +#: forms.py:41 +msgid "email address" +msgstr "direcciцЁn de coreo electrцЁnico" + +#: forms.py:43 +msgid "password" +msgstr "contraseц╠a" + +#: forms.py:45 +msgid "password (again)" +msgstr "contraseц╠a (otra vez)" + +#: forms.py:54 +msgid "Usernames can only contain letters, numbers and underscores" +msgstr "Los nombres de usuarios sцЁlo pueden contener letras, nц╨meros y guiones bajos" + +#: forms.py:59 +msgid "This username is already taken. Please choose another." +msgstr "Este nombre de usuario ya estц║ ocupado. Por favor escoge otro" + +#: forms.py:71 +msgid "You must type the same password each time" +msgstr "Tienes que introducir la misma contraseц╠a cada vez" + +#: forms.py:100 +msgid "I have read and agree to the Terms of Service" +msgstr "He leц╜do y acepto los tц╘rminos de servicio" + +#: forms.py:109 +msgid "You must agree to the terms to register" +msgstr "Tienes que aceptar los tц╘rminos para registrarte" + +#: forms.py:128 +msgid "" +"This email address is already in use. Please supply a different email " +"address." +msgstr "" +"La direcciцЁn de correo electrцЁnico ya estц║ siendo usada. Por favor" +"proporciona otra direcciцЁn." + +#: forms.py:153 +msgid "" +"Registration using free email addresses is prohibited. Please supply a " +"different email address." +msgstr "" +"El registro usando una direcciцЁn de correo electrцЁnico gratis estц║ prohibido." +"Por favor proporciona otra direcciцЁn." + +#: models.py:188 +msgid "user" +msgstr "usuario" + +#: models.py:189 +msgid "activation key" +msgstr "clave de activaciцЁn" + +#: models.py:194 +msgid "registration profile" +msgstr "perfil de registro" + +#: models.py:195 +msgid "registration profiles" +msgstr "perfiles de registro" diff --git a/registration/locale/es_AR/LC_MESSAGES/django.mo b/registration/locale/es_AR/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..ce8b4e56c8250021193dd307b05b803d3fbba025 GIT binary patch literal 1849 zcmb7^&2Jk;7{&)E6wFttKp=s{dus!%aX^K{O{18^ZLQi#&=%4NF8bRH{P9j=ACEW*AFgV_)4H% zLVpeYYxI}H_z^tN?t>9H13v=41vkNqj|%Y~xCdSbzXY#3R>7aZ9{4A?2HtpL zzW--n(EqOHFW}So{X6&y_yD{N{#Ac(T*R61`*l2C0Iz`9zG@->N8%jtXlgYF~LynzWsLqYE6<1pDVc@vW~9QN{IGIZVztcqw33(iSRV_;6Jrpb)CPyPO|e_Aa1={TMkD6f zXfLsU#@@4aZ;>wZeGDqkN}XUB8ysHTH6`Vx^Jk638W;&zZDk&g zAw8lE>A6ksv|8^+t>tKOnHHDZ@3x}WwN?ugqajaK5U=Om?RJLIU@O|_(Cw}Ep!ep7 zTbtcRUpgO+tkf=(-q<$X-0k;ivoq+@N_Wud_18KZ8^i9-&MKzrw3I1FBbIra`kl>N zyCLM2et#`fO=Hv5#-KOoo*Zhi*=nrgUXh2<2)EEC&-eY?MF#(_Q#_H@vA?l9x*08< z%nPv~ccZS3O@jPy(-M-rA#O8r%_1!R);_8YC)-qqpGe_++wMaJrF+p=BTm~i3B#5#&U`$T)&pt9gPPzbxnGihtQH{sFN^CdB{% literal 0 HcmV?d00001 diff --git a/registration/locale/es_AR/LC_MESSAGES/django.po b/registration/locale/es_AR/LC_MESSAGES/django.po new file mode 100644 index 0000000..fb746b5 --- /dev/null +++ b/registration/locale/es_AR/LC_MESSAGES/django.po @@ -0,0 +1,83 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2008 Leonardo Manuel Rocha +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-19 19:30-0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: forms.py:38 +msgid "username" +msgstr "nombre de usuario" + +#: forms.py:41 +msgid "email address" +msgstr "direcciцЁn de e-mail" + +#: forms.py:43 +msgid "password" +msgstr "contraseц╠a" + +#: forms.py:45 +msgid "password (again)" +msgstr "contraseц╠a (nuevamente)" + +#: forms.py:54 +msgid "Usernames can only contain letters, numbers and underscores" +msgstr "El nombre de usuario solo puede contener letras, nц╨meros y guiones bajos" + +#: forms.py:59 +msgid "This username is already taken. Please choose another." +msgstr "Ese nombre de usuario ya estц║ asignado. Por favor elija otro." + +#: forms.py:68 +msgid "You must type the same password each time" +msgstr "Debe tipear la misma contraseц╠a cada vez" + +#: forms.py:96 +msgid "I have read and agree to the Terms of Service" +msgstr "He leц╜do y estoy de acuerdo con las Condiciones de Servicio" + +#: forms.py:105 +msgid "You must agree to the terms to register" +msgstr "Debe estar de acuerdo con las Condiciones para poder registrarse" + +#: forms.py:124 +msgid "" +"This email address is already in use. Please supply a different email " +"address." +msgstr "Esa direcciцЁn de e-mail ya estц║ en uso. Por favor provea otra " +"direcciцЁn." + +#: forms.py:149 +msgid "" +"Registration using free email addresses is prohibited. Please supply a " +"different email address." +msgstr "La registraciцЁn con un e-mail gratuito estц║ prohibida. Por favor " +"de una direcciцЁn de e-mail diferente." + +#: models.py:188 +msgid "user" +msgstr "usuario" + +#: models.py:189 +msgid "activation key" +msgstr "clave de activaciцЁn" + +#: models.py:194 +msgid "registration profile" +msgstr "perfil de registro" + +#: models.py:195 +msgid "registration profiles" +msgstr "perfiles de registro" diff --git a/registration/locale/fr/LC_MESSAGES/django.mo b/registration/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..f8911c7ec65960e02435383ea87d2d79adc420db GIT binary patch literal 1883 zcmb7^%WfP+6oxAh2n_cK#DXB5-NcX{+KvEwVxk0-fRPQBGq%JERl3gfl+#_+)McHPwJu=c?WJODojAAz5MmmU-19dH-C0e%bi!C%1z@GtOr@U_Q@$I z%iuQn3Rr>{!LPt{?rU%zvAzLU@%=mgSb~?Iz&`j8O#8opTi}H!g;)c(!LzhzY*T(Ov^(8Tx<|4}E7>5`cBTA} z7u*O)A&_RPM-O}&k65RPoH99VPCEJsz`T6Xp#EVl;u}oU`Og zRw=y$(kUkYUw1HZM%H}RQ;-kXoH!~b)*?M=EH3Q^V&}-hQy~pmgPaR%f<*j^Lty8x zk%{$vytJA{qwrc-TwZ);Bh}G|c_Co~A)%^k1!Q-6E*vz_8)EyEV)gq)_{lWD;A>?r{;ab(QVn zTB!7p)Sfh@&E|aOgX`4W=Ek~^ZTeEH?EP5!Y-ESj+xm2rt)XbCPqGhW;2|yL`MX)Z zk}a;#;>z%Pp5<@nISyvqJk=@6xpPZ7E#^4)X1<7X8`6htD=f?2$x6t&ZKTN)N0nZ}v9VH}9NYba9aP z)=+EoW;Q})h7@=|ywz0D^ad3Z={$$`c1Cxz<&Fs+0w5|F(L08(hs)>!hHokDWX=e>HT2M4-(r(0# zhD7zFL2uSgU@B4Ts4-G#K8+hV#K5rZa@1AD4~Y%7mHr~K>paEacp%nNN1t<>T&UUM z-t0$;ZA!Dl|Ky1p&r#Vng>&}viD2ST^OOh4E{zTJerJqJHXK7ca%^x28IyuOY4$K; z*H8xh|G--kwqgva!;H*+a+o0-D_ug-f!MVXlJXQc@5INolggOnWAUNhso>Pu=xCa( zI^i9Q6YWlfL%4eOGX}M`kf@tiWQPW5ohy#}sY~j{i=slgRsVE+#a!*gSr2m`bajfB g&|_V+ov4mQ|9dWdoy5{We@r5`#(djAv0Y}F-M*si- literal 0 HcmV?d00001 diff --git a/registration/locale/fr/LC_MESSAGES/django.po b/registration/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 0000000..34b520b --- /dev/null +++ b/registration/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,81 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Samuel Adam , 2007. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: django-registration 0.3 \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-19 19:30-0500\n" +"PO-Revision-Date: 2007-09-20 10:30+0100\n" +"Last-Translator: Samuel Adam \n" +"Language-Team: Franц╖ais \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: forms.py:38 +msgid "username" +msgstr "pseudo" + +#: forms.py:41 +msgid "email address" +msgstr "adresse email" + +#: forms.py:43 +msgid "password" +msgstr "mot de passe" + +#: forms.py:45 +msgid "password (again)" +msgstr "mot de passe (vц╘rification)" + +#: forms.py:54 +msgid "Usernames can only contain letters, numbers and underscores" +msgstr "Le pseudo ne peut contenir que des lettres, chiffres et le caractц╗re soulignц╘." + +#: forms.py:59 +msgid "This username is already taken. Please choose another." +msgstr "Ce pseudo est dц╘jц═ utilisц╘. Veuillez en choisir un autre." + +#: forms.py:68 +msgid "You must type the same password each time" +msgstr "Veuillez indiquer le mц╙me mot de passe dans les deux champs" + +#: forms.py:96 +msgid "I have read and agree to the Terms of Service" +msgstr "J'ai lu et acceptц╘ les Conditions Gц╘nц╘rales d'Utilisation" + +#: forms.py:105 +msgid "You must agree to the terms to register" +msgstr "Vous devez accepter les conditions d'utilisation pour vous inscrire" + +#: forms.py:124 +msgid "" +"This email address is already in use. Please supply a different email " +"address." +msgstr "Cette adresse email est dц╘jц═ utilisц╘e. Veuillez en indiquer une autre." + +#: forms.py:149 +msgid "" +"Registration using free email addresses is prohibited. Please supply a " +"different email address." +msgstr "L'inscription avec une adresse email d'un compte gratuit est interdite. Veuillez en indiquer une autre." + +#: models.py:188 +msgid "user" +msgstr "utilisateur" + +#: models.py:189 +msgid "activation key" +msgstr "clц╘ d'activation" + +#: models.py:194 +msgid "registration profile" +msgstr "profil d'inscription" + +#: models.py:195 +msgid "registration profiles" +msgstr "profils d'inscription" diff --git a/registration/locale/he/LC_MESSAGES/django.mo b/registration/locale/he/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..be936503238a6df8192233f1fe242e7e60d41895 GIT binary patch literal 1896 zcmb7C&u<$=6ds@uFh9i)Ayf|UMpL@R2?7Nd5KxMUs38h&MGqC4>^`rT*t^!uIIRy{ zxFRG+wEM2 z%$G5Tm@mru0sJs-00UqL_$BZF_$hGdL8aaVUICs5eg}L7_#1E;_!sag;OU2ydJ=dR z_$Y7 z1HOfA5BMDLci>CFd%yzl@9Fu{(xlGQK)HV%DEAfM)4(r)2%ith{|QXVPwtL})baRq z16qYt^C9^k4bUh^p^#>-hs1acQ>{~7?=TtG5$O~&Hq7KQa&@LEo2I3#MxV3UNkUee z94FQp?UF2|woTHQYGTh#ois>Ckzs4uQevrPvU;+WIF5?6(O_+vY`fKJbVyT_)M{)v zb&HNgRjtEwflR1$L(=O|A~E^@(!s@~y2Aryg*v4y zMb2TCI*s@Zc8-lbM`^pcg+;DeJB_dmGZe3`W^HP=ty>flJ`fVBnp{9OGdtI5L2rm{ zCC8Sw_IhR_Vjb4WB~4a3bji*{@ony?1$ESs{qcg%=opDEt4XA3tI&*E(%=&htrCUu zpU9*(O!gIrE?AF(D<}({E>F{*kQFw$l^GXQY@9^F2kqDf)vQc~jnAsV1vEF-e^p z3BBFK?!D%=&1$X~X2qoOZsC$n0{n9e7#UvuUwp!vQjJ+E})xueL)r4 zm&x%B_gbp~)y`8`*T%AY=W_L7t=6{@aoI7hNrFkfj{(JKlTIiec$tU z{Vl&Y*$x4BaEF_oANc{!`+lUbImBj!&293#{%8L^4#p%-*kW-LS6zQd_zl7TBd+^k z0?!XHxdWbom~|1X?+=S4Q3$4Y{k}gSBrwK#&eVkmxQlB3ur~|PyFbJjBz;izglPA_ z>E{wXiZha;dPs4EEby1Ia>D!Qc99#>mpb5#6i2y0C~VGofF$}TZ6LHm5Z}UbNGJ-0 zc5x-5;eU?u>P09J^r2cGU+y&qy$AvU{cQ!gh3%&an{;bReR8kRr-|i3?sijx=F&@K zNF)s(VD8CgALP=TJkB1r2rVXZj@;Eu_kT53%~, 2008. +# , fuzzy +# <>, 2008. +# +# +msgid "" +msgstr "" +"Project-Id-Version: registration\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2008-02-10 02:01+0200\n" +"PO-Revision-Date: 2008-02-10 02:05+0200\n" +"Last-Translator: Meir Kriheli \n" +"Language-Team: Hebrew\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit" + +#: forms.py:38 +msgid "username" +msgstr "в╘в² в·в╘в╙в·в╘" + +#: forms.py:41 +msgid "email address" +msgstr "в⌠в∙в░в╗ в░в°в╖в≤в╗в∙в═в≥" + +#: forms.py:43 +msgid "password" +msgstr "в║в≥в║в·в■" + +#: forms.py:45 +msgid "password (again)" +msgstr "в║в≥в║в·в■ (в╘в∙в▒)" + +#: forms.py:54 +msgid "Usernames can only contain letters, numbers and underscores" +msgstr "в╘в·в∙в╙ в·в╘в╙в·в╘ в≥в⌡в∙в°в≥в² в°в■в⌡в≥в° в╗в╖ в░в∙в╙в≥в∙в╙, в║в╓в╗в∙в╙ в∙в╖в∙в∙в≥в² в╙в≈в╙в∙в═в≥в²" + +#: forms.py:59 +msgid "This username is already taken. Please choose another." +msgstr "в╘в² в■в·в╘в╙в·в╘ в╙в╓в∙в║ в⌡в▒в╗. в═в░ в°в▒в≈в∙в╗ в░в≈в╗." + +#: forms.py:64 +msgid "You must type the same password each time" +msgstr "в≥в╘ в°в■в╖в°в≥в⌠ в░в╙ в░в∙в╙в■ в■в║в≥в║в·в■ в╓в╒в·в≥в≥в²" + +#: forms.py:93 +msgid "I have read and agree to the Terms of Service" +msgstr "в╖в╗в░в╙в≥ в∙в■в║в⌡в·в╙в≥ в°в╙в═в░в≥ в■в╘в≥в·в∙в╘" + +#: forms.py:102 +msgid "You must agree to the terms to register" +msgstr "в╒в°в≥в  в°в■в║в⌡в≥в² в°в╙в═в░в≥ в■в╘в≥в·в∙в╘" + +#: forms.py:121 +msgid "" +"This email address is already in use. Please supply a different email " +"address." +msgstr "" +"в⌡в╙в∙в▒в╙ в■в⌠в∙в░в╗ в■в░в°в╖в≤в╗в∙в═в≥ в╙в╓в∙в║в■ в⌡в▒в╗. в═в░ в°в║в╓в╖ в⌡в╙в∙в▒в╙ в⌠в∙в░в╗ в░в≈в╗в╙." + +#: forms.py:146 +msgid "" +"Registration using free email addresses is prohibited. Please supply a " +"different email address." +msgstr "" +"в■в╗в≥в╘в∙в² в▒в╒в√в╗в╙ в╙в≥в▒в╙ в⌠в∙в░в╗ в░в°в╖в≤в╗в∙в═в≥ в≈в≥в═в·в≥в╙ в░в║в∙в╗. в═в░ в°в║в╓в╖ в⌡в╙в∙в▒в╙ в░в≈в╗в╙." + +#: models.py:188 +msgid "user" +msgstr "в·в╘в╙в·в╘" + +#: models.py:189 +msgid "activation key" +msgstr "в·в╓в╙в≈ в■в╓в╒в°в■" + +#: models.py:194 +msgid "registration profile" +msgstr "в╓в╗в∙в╓в≥в° в╗в≥в╘в∙в²" + +#: models.py:195 +msgid "registration profiles" +msgstr "в╓в╗в∙в╓в≥в°в≥ в╗в≥в╘в∙в²" + diff --git a/registration/locale/it/LC_MESSAGES/django.mo b/registration/locale/it/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..83ec9ddb1646693415fb7b3a37786f84d642c655 GIT binary patch literal 1864 zcmb7^OLH7G5XS`x1eRyDM_5DFa>`A zqut-Z7r;NkX!im5GWa+6I(X*Xgcm@3Qp?~o;8hUUw=MerEPC`W+M2caj<`3rcowd< zE#5P0Fry%af}5=#9q?&%wM7FS2|3|{*r1nAgo33o5bBE?da?&}OWa83qV}ZJJ~$5A z8fv^YB^|{5GGtv57ljk=#gq0_J3G+(I!KYyZYAu6d{fuen3xKEa3D@hIDIfx{Q*v% zG8Rp772O+?j(5tVn!^!icqk_=1wIrrHh4w3;=hz%aLXy!yg6G;Dl0EhNrBNT}*s0mU8f3*#DkLu?;?tl9gott&`4 zA5hRkQJe=onkarKV|7X$HQPTQ&;plmbWu%inuZF&IM9{+??UOUr-PNSG zO1;(Woo>=y?sl;;*^`luHjnqMU@cj?LcQgzyL_>Wlbvn$A?Z6dzT#kAMmKeCE2eFB z)-zo-n0^rCFn&&rg!gd5lns}X^o55RZ3V70;@R2R+SxpQMlbDlt|5}hyrd7^GYWDq zT&gQ5wnq7YotLnFyMH5DIo=oDK(;2ECbtDLKBE<+b?5WsBQXMfqko@l@V->kOMAWa za%Z<{99PK=i=byjrf%nceQAv*!}SG2Z>&>qajm1S>wKW`E))f!5?z~OmZY862;7LB z;3L8DLLWgo9hcgQ#7u4TG^rfNMTx13N-4C{M@M>EOl8aiPXDfRtdCPir<$&|6k^d?3@=4KiXB~!YUOIc~W zV&+eZe<_#txIX^5-);$-iBbKDGrT8RzulYNg%0dUwc~` zCO#=Kq?<~tHm2*kRVYS)$%m-gagD-&rje}zs~AK4jm8*;Sgvg-)I*xhi*!Dq1)MOA d9oa=SsnIqMW{ys*OcvUxpI(?qqE5)E{sRa2Dzg9p literal 0 HcmV?d00001 diff --git a/registration/locale/it/LC_MESSAGES/django.po b/registration/locale/it/LC_MESSAGES/django.po new file mode 100644 index 0000000..00129b0 --- /dev/null +++ b/registration/locale/it/LC_MESSAGES/django.po @@ -0,0 +1,82 @@ +# translation of django.po to Italiano +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Nicola Larosa , 2008. +msgid "" +msgstr "" +"Project-Id-Version: django\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-19 19:30-0500\n" +"PO-Revision-Date: 2008-05-27 15:05+0200\n" +"Last-Translator: Nicola Larosa \n" +"Language-Team: Italiano\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: KBabel 1.11.4\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: forms.py:38 +msgid "username" +msgstr "nome utente" + +#: forms.py:41 +msgid "email address" +msgstr "indirizzo email" + +#: forms.py:43 +msgid "password" +msgstr "password" + +#: forms.py:45 +msgid "password (again)" +msgstr "password (di nuovo)" + +#: forms.py:54 +msgid "Usernames can only contain letters, numbers and underscores" +msgstr "I nomi utente possono contenere solo lettere, numeri e sottolineature" + +#: forms.py:59 +msgid "This username is already taken. Please choose another." +msgstr "Questo nome utente ц╗ giц═ usato. Scegline un altro." + +#: forms.py:68 +msgid "You must type the same password each time" +msgstr "Bisogna inserire la stessa password ogni volta" + +#: forms.py:96 +msgid "I have read and agree to the Terms of Service" +msgstr "Dichiaro di aver letto e di approvare le Condizioni di Servizio" + +#: forms.py:105 +msgid "You must agree to the terms to register" +msgstr "Per registrarsi bisogna approvare le condizioni" + +#: forms.py:124 +msgid "This email address is already in use. Please supply a different email " +"address." +msgstr "Questo indirizzo email ц╗ giц═ in uso. Inserisci un altro indirizzo email." + +#: forms.py:149 +msgid "Registration using free email addresses is prohibited. Please supply a " +"different email address." +msgstr "La registrazione con indirizzi email gratis non ц╗ permessa. " +"Inserisci un altro indirizzo email." + +#: models.py:188 +msgid "user" +msgstr "utente" + +#: models.py:189 +msgid "activation key" +msgstr "chiave di attivazione" + +#: models.py:194 +msgid "registration profile" +msgstr "profilo di registrazione" + +#: models.py:195 +msgid "registration profiles" +msgstr "profili di registrazione" + diff --git a/registration/locale/ja/LC_MESSAGES/django.mo b/registration/locale/ja/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..e0332b0e7cce033c22f9b774b117cf5d78e6becd GIT binary patch literal 2035 zcmbu8%WoS+9LEPJl$!Do0)YgF;Y0(hS+_hAH&lg^Ng zr0i}=X&!2uCWHV|X@I08O+zW~0y!WdapjD-O#I-;f53OPb{s=fff#B0*`578zrXSC z_ivjaSodK+iv0ohL+be!{9p}(8dw5f0~f(p!R@ya@*H>++zU>EkAuH}ZQ!5aJ>Vm^ z5pp;91b8Rd4ys%)f)9ZG;DewEZUf&0m0bWHM6PjA<@psK$G{!86H)}H!3-F?gOC*X zI@ksN40eJ$w-fRh*bS=qgW$cO4?YZ@t#hf)pTHeB{|(#;RzReWma1Q!srpom#!`LU zRewQRf>NWU>TfKBA&`Qg+(?fYsPd3*nq@G%+A%Bj09(To4)Gp?a;~~y6`(i77OKVq6XFfWO;gT{EcL8BC(Q7yq3end@)BuLhcxUr9_TJ+qR=*S zJyU&D7)Hj*@f#irq7;gLluV@0bq2WMY29hoC;%`H-cu zH0|0oJ)!MM(1f1ctv{)Ez^;c0PfNMX7CGiQZjv6(T6T%iLxY$a?HnE!St0h97$zAv zocKUtA6#s+$V{%KILjyLE3Ck5jxl%k^z6@BamO|H#d^DYyP91l;(F`=291lUr7)Hx z_4u&&bRh>p_fjLvT*18;j-(E1?M*vX6E3sXWgAW!e~2V)$E}YM8GI?j?`3#S2H%$7 z{4fc1WICZ{zRkiMd_oy#_$^R zn3DlIIxhns5+QVb^8RvT;L4!9cA~Zb0}L#X!B{g2jHLgr49}{8hpW*oIEBfVPkd4y zdtcp?n>ec?1NgipgO6l57Zv}t3>RgH`d9EuZg>@N+LG???{rsn{=3@JWE-jX7|zNN X;V}0LGWb}A%Uget4XF{dDLVNJiZWT^ literal 0 HcmV?d00001 diff --git a/registration/locale/ja/LC_MESSAGES/django.po b/registration/locale/ja/LC_MESSAGES/django.po new file mode 100644 index 0000000..afaaf94 --- /dev/null +++ b/registration/locale/ja/LC_MESSAGES/django.po @@ -0,0 +1,78 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Shinya Okano , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: django-registration 0.4 \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-19 19:30-0500\n" +"PO-Revision-Date: 2008-01-31 10:20+0900\n" +"Last-Translator: Shinya Okano \n" +"Language-Team: Japanese \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: forms.py:38 +msgid "username" +msgstr "Ц┐╕Ц┐╪Ц┌╤Е░█" + +#: forms.py:41 +msgid "email address" +msgstr "Ц┐║Ц┐╪Ц┐╚Ц┌╒Ц┐┴Ц┐╛Ц┌╧" + +#: forms.py:43 +msgid "password" +msgstr "Ц┐▒Ц┌╧Ц┐╞Ц┐╪Ц┐┴" + +#: forms.py:45 +msgid "password (again)" +msgstr "Ц┐▒Ц┌╧Ц┐╞Ц┐╪Ц┐┴ (Г╒╨Х╙█)" + +#: forms.py:54 +msgid "Usernames can only contain letters, numbers and underscores" +msgstr "Ц┐╕Ц┐╪Ц┌╤Е░█Ц│╚Ц│╞Е█┼Х╖▓Х▀╠Ф∙╟Ц│╗Ц┌╒Ц┐ЁЦ┐─Ц┐╪Ц┌╧Ц┌ЁЦ┌╒Ц│╝Ц│©Ц│▄Д╫©Г■╗Ц│╖Ц│█Ц│╬Ц│≥Ц─┌" + +#: forms.py:59 +msgid "This username is already taken. Please choose another." +msgstr "Ц│⌠Ц│╝Ц┐╕Ц┐╪Ц┌╤Е░█Ц│╞Ф≈╒Ц│╚Д╫©Г■╗Ц│∙Ц┌▄Ц│╕Ц│└Ц│╬Ц│≥Ц─┌Д╩√Ц│╝Ц┐╕Ц┐╪Ц┌╤Е░█Ц┌▓Ф▄┤Е╝ Ц│≈Ц│╕Ц│▐Ц│═Ц│∙Ц│└Ц─┌" + +#: forms.py:68 +msgid "You must type the same password each time" +msgstr "Е░▄Ц│≤Ц┐▒Ц┌╧Ц┐╞Ц┐╪Ц┐┴Ц┌▓Е┘╔Е┼⌡Ц│≥Ц┌▀Е©┘Х╕│Ц│▄Ц│┌Ц┌┼Ц│╬Ц│≥Ц─┌" + +#: forms.py:96 +msgid "I have read and agree to the Terms of Service" +msgstr "Ц┌╣Ц┐╪Ц┐⌠Ц┌╧Е┬╘Г■╗Х╕▐Г╢└Ц┌▓Х╙╜Ц│©Ц─│Е░▄Ф└▐Ц│≈Ц│╬Ц│≥Ц─┌" + +#: forms.py:105 +msgid "You must agree to the terms to register" +msgstr "Г≥╩И▄╡Ц│≥Ц┌▀Ц│÷Ц┌│Ц│╚Ц│╞Х╕▐Г╢└Ц│╚Е░▄Ф└▐Ц│≥Ц┌▀Е©┘Х╕│Ц│▄Ц│┌Ц┌┼Ц│╬Ц│≥Ц─┌" + +#: forms.py:124 +msgid "This email address is already in use. Please supply a different email address." +msgstr "Ц│⌠Ц│╝Ц┐║Ц┐╪Ц┐╚Ц┌╒Ц┐┴Ц┐╛Ц┌╧Ц│╞Ф≈╒Ц│╚Д╫©Г■╗Ц│∙Ц┌▄Ц│╕Ц│└Ц│╬Ц│≥Ц─┌Д╩√Ц│╝Ц┐║Ц┐╪Ц┐╚Ц┌╒Ц┐┴Ц┐╛Ц┌╧Ц┌▓Ф▄┤Е╝ Ц│≈Ц│╕Д╦▀Ц│∙Ц│└Ц─┌" + +#: forms.py:149 +msgid "Registration using free email addresses is prohibited. Please supply a different email address." +msgstr "Х┤╙Г■╠Ц│╙Ц┐║Ц┐╪Ц┐╚Ц┌╒Ц┐┴Ц┐╛Ц┌╧Ц┌▓Д╫©Г■╗Ц│≈Ц│÷Г≥╩И▄╡Ц│╞Г╕│Ф╜╒Ц│∙Ц┌▄Ц│╕Ц│└Ц│╬Ц│≥Ц─┌Д╩√Ц│╝Ц┐║Ц┐╪Ц┐╚Ц┌╒Ц┐┴Ц┐╛Ц┌╧Ц┌▓Ф▄┤Е╝ Ц│≈Ц│╕Ц│▐Ц│═Ц│∙Ц│└Ц─┌" + +#: models.py:188 +msgid "user" +msgstr "Ц┐╕Ц┐╪Ц┌╤" + +#: models.py:189 +msgid "activation key" +msgstr "Ц┌╒Ц┌╞Ц┐├Ц┌ёЦ┐≥Ц┐╪Ц┌╥Ц┐╖Ц┐ЁЦ┌╜Ц┐╪" + +#: models.py:194 +msgid "registration profile" +msgstr "Г≥╩И▄╡Ц┐≈Ц┐╜Ц┐∙Ц┌║Ц┌╓Ц┐╚" + +#: models.py:195 +msgid "registration profiles" +msgstr "Г≥╩И▄╡Ц┐≈Ц┐╜Ц┐∙Ц┌║Ц┌╓Ц┐╚" + diff --git a/registration/locale/nl/LC_MESSAGES/django.mo b/registration/locale/nl/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..9e84eb3ed60d06c9e37b1d48fa0edf3b5410f230 GIT binary patch literal 1898 zcmbW1&u<$=6vqcBl$xKaia5aGAr2(aHBJgB+9XxeG>rmINrO{_IH2+F^Lpalnbpj! zT~~qw2QG*sH}3o$9QhA8@h5<|bKpC>c5I^xsf@h-%*?)d^XB{J?eAAF{V330#q%bf zpYXgM`cL5x?I9R}Q}8SBXYd|)`Dr0O0v~|e;1A$C;IH61_!syx_{K9ryac`rJ`Zkz zVXr;#8h8r624>(T@M|#GeG4Yo>wEA9#(&~vAAI{+A?}0UfuDoFfyZFyIU#O?-+}8h91s_1Sm_yn^w4FvR^Oi0$hZ`~)8%zE|p*<}aM%h1nSH z1wyS`h=1PTMnDRIFl)bc4B_Pw4ds~0vP?;8Jeg%CZ^#dsdTa|vW{aF2q<$=O)Ic z#4;I@R|N~1cr|VmAMsSIs3Yd~7adxY83J7w%}%paAsGXe^J6ES2nyAIK_+ox%{eDN zI!dDlI2JM;&%$n;wNAM-)ai?h>h9e;Jli`wpq*~_R<7E{W;a{MN5=OK~e{RH0%j(u(~&BcY0@ zz?g$|ND%5LnpGq2W1Buv4ACkSk@HnQS3GEpQS5A2ZN(jhcT1~7$)LPb6m$ryZ!0x= zLXSAfd?hZEma_OCPXFzqEe@MtYL^9$4rw$X^E?Q#T%|=gC8kuCF^)|5d@+~j_(=5` zWrfrSX$u1lXHr9TlzkBO@%b?Yd!g#4mYo, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: registration\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2008-08-14 13:25+0200\n" +"PO-Revision-Date: 2008-08-14 13:25+0200\n" +"Last-Translator: Joost Cassee \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: forms.py:38 +msgid "username" +msgstr "gebruikersnaam" + +#: forms.py:41 +msgid "email address" +msgstr "e-mail adres" + +#: forms.py:43 +msgid "password" +msgstr "wachtwoord" + +#: forms.py:45 +msgid "password (again)" +msgstr "wachtwoord (opnieuw)" + +#: forms.py:54 +msgid "Usernames can only contain letters, numbers and underscores" +msgstr "Gebruikersnamen kunnen alleen letters, nummer en liggende streepjes bevatten." + +#: forms.py:59 +msgid "This username is already taken. Please choose another." +msgstr "Deze gebruikersnaam is reeds in gebruik. Kiest u alstublieft een andere gebruikersnaam." + +#: forms.py:71 +msgid "You must type the same password each time" +msgstr "U moet twee maal hetzelfde wachtwoord typen." + +#: forms.py:100 +msgid "I have read and agree to the Terms of Service" +msgstr "Ik heb de servicevoorwaarden gelezen en ga akkoord." + +#: forms.py:109 +msgid "You must agree to the terms to register" +msgstr "U moet akkoord gaan met de servicevoorwaarden om u te registreren." + +#: forms.py:125 +msgid "This email address is already in use. Please supply a different email address." +msgstr "Dit e-mail adres is reeds in gebruik. Kiest u alstublieft een ander e-mail adres." + +#: forms.py:151 +msgid "Registration using free email addresses is prohibited. Please supply a different email address." +msgstr "U kunt u niet registreren met een gratis e-mail adres. Kiest u alstublieft een ander e-mail adres." + +#: models.py:191 +msgid "user" +msgstr "gebruiker" + +#: models.py:192 +msgid "activation key" +msgstr "activatiecode" + +#: models.py:197 +msgid "registration profile" +msgstr "registratieprofiel" + +#: models.py:198 +msgid "registration profiles" +msgstr "registratieprofielen" diff --git a/registration/locale/pl/LC_MESSAGES/django.mo b/registration/locale/pl/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..1f2a228861c28105aae351cdeded9976e68bbd76 GIT binary patch literal 1769 zcmb7^&u=3&6vquLEE|3=AP#VNH@46TN!hY(cByt*id58=s-%nHf}EL`iIW*SvOUet zNN}h?NFWXzIH2Xmf&YM0xits)12}Ty58yxG>q+`cMMzk2J|53|&(Ggy&!1N>eJ{|S zMSlhTNA#D%_#ynE-3KGE1V06T2Dib>4-4@Ycn`b@eha<|{su09e}hkhFFzv0Q{Zdh zf+=_j{0t0s2Vfg(eFKKLzv5+B|1WS2Tz^!E8{ik z!t)*QIWPmC06z!u6JO8gzXPx0`6uvs@E0)H{XU=n1H|%m3;92Z9`bvpo|*lHeFUG- zU@nkq-9rAy?J@?CLLj|bj|O-JU2M=m?lW1I2}zBfT4wTw{D7&;Hg{zD^dZ}Q6|>mo zR5@>@S4LCeluoH1>^YYzBbg+YonuGJ(b$@S>M74jlXfzej>#3{aaNL~MD_b@S^M*j zP0<~|^IWhHN@gLwl2lle|8F?BSS@ot8_CNN>ytq7z!;<_wZW!sQ|!$>9K}+T(a1SA z+DojTvG;7IgX`s z6Jrx%84t*-oP~_N+Mg*t;!>PbM;zOq4CtCnk?4Y$tu!wciqTgY|Ldd^L81CD$Ru{G z8FK8SjU>8 zwN|6{Dz#TTZ?>Y=a;pWyXqWd@2zb`6y%e>VslC*>zWjQt4ZBV0eAKm4yG(jxJM_M^ zJfcrhlSsNX3=hpJJlsyhscXh2UxSlQ3z>4%Wtn$q$7IS4=+@@u?MyX|P1hP*8(ZtA z8)`ROjk~BR&S2C<1v=#UzJFt!LA#q252SVM-`?xK7p&c{ zC{}S?m0nH{I1eQ*GMVGBD3YU?$G#XI{XnS^+lncdl$+^crY2_kAXcy*a=20PP~pl{ zB^A@dGFAm8(&lEuslm0v!766gqDgyrKg3ng7hZ`j>zVQV3WdltM8G=eMC$Ut-6d&R zO=NBDM`ohcNYZJLN@5>gHtD0Xr`T2@iK8!@V*8>~*dkLUC$N)8Uy(1fk)aG<16wXq zT=p$)GgVrSuxpcyEKeU~td$g7_^P-Ht~2S(B)FeQx=6BDl2g#L!a=B|t4)smCS&F5 z1x>h51|K&!JuoyO(<`u2hFc{@SrJz#jV~uME{D1lwZb#e#LQz3x;8dC2*C?tc8AUu UY(5pfo+m{Vp?-7m#6>y9KORXKn*aa+ literal 0 HcmV?d00001 diff --git a/registration/locale/pl/LC_MESSAGES/django.po b/registration/locale/pl/LC_MESSAGES/django.po new file mode 100644 index 0000000..498fd5b --- /dev/null +++ b/registration/locale/pl/LC_MESSAGES/django.po @@ -0,0 +1,84 @@ +# Polish translation for django-registration. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the django-registration package. +# Jarek Zgoda , 2007. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: 0.4\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-19 19:30-0500\n" +"PO-Revision-Date: 2007-12-15 12:45+0100\n" +"Last-Translator: Jarek Zgoda \n" +"Language-Team: Polish \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: forms.py:38 +msgid "username" +msgstr "nazwa uе╪ytkownika" + +#: forms.py:41 +msgid "email address" +msgstr "adres email" + +#: forms.py:43 +msgid "password" +msgstr "hasе┌o" + +#: forms.py:45 +msgid "password (again)" +msgstr "hasе┌o (ponownie)" + +#: forms.py:54 +msgid "Usernames can only contain letters, numbers and underscores" +msgstr "" +"Nazwa uе╪ytkownika moе╪e zawieraд┤ tylko litery, cyfry i znaki podkreе⌡lenia" + +#: forms.py:59 +msgid "This username is already taken. Please choose another." +msgstr "Ta nazwa uе╪ytkownika jest juе╪ zajд≥ta. Wybierz innд┘." + +#: forms.py:68 +msgid "You must type the same password each time" +msgstr "Musisz wpisaд┤ to samo hasе┌o w obu polach" + +#: forms.py:96 +msgid "I have read and agree to the Terms of Service" +msgstr "Przeczytaе┌em regulamin i akceptujд≥ go" + +#: forms.py:105 +msgid "You must agree to the terms to register" +msgstr "Musisz zaakceptowaд┤ regulamin, aby siд≥ zarejestrowaд┤" + +#: forms.py:124 +msgid "" +"This email address is already in use. Please supply a different email " +"address." +msgstr "Ten adres email jest juе╪ uе╪ywany. Uе╪yj innego adresu email." + +#: forms.py:149 +msgid "" +"Registration using free email addresses is prohibited. Please supply a " +"different email address." +msgstr "" +"Nie ma moе╪liwoе⌡ci rejestracji przy uе╪yciu darmowego adresu email. Uе╪yj " +"innego adresu email." + +#: models.py:188 +msgid "user" +msgstr "uе╪ytkownik" + +#: models.py:189 +msgid "activation key" +msgstr "klucz aktywacyjny" + +#: models.py:194 +msgid "registration profile" +msgstr "profil rejestracji" + +#: models.py:195 +msgid "registration profiles" +msgstr "profile rejestracji" diff --git a/registration/locale/pt_BR/LC_MESSAGES/django.mo b/registration/locale/pt_BR/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..e81b6207be3808b1553a40605d4c5f7ea270a843 GIT binary patch literal 1796 zcmbu9O>Y}T7{>=FZ*HMbKuCzgb0ZO5;}i;wTdJC5Qw)BI96M5xS~T5#yqdFT=>u0aT1jaVx-yMdOXj|{O5W0w{xey5*TMO zU&s6s^Ht0pMqb5J@7B^U2x}dAufZTgKvO8g6rV#;Pc?=Cxmzod<}d7 zTmxSQH^EoH9(W2&!Dm1PUW9K8Ucm2%_)~+wfH%R9o?OIx2yWx|AK*LS`cpz|fVaSx zz%Rga;J4t5;1A37pTQ9SS1`o;bIHF!gssLIJWpeWypE5cQ8-f=*e;M_HG-eR@ibm= zbb++0bJxIUF-4mua>is?rX)3HW|_$w@)M>Z+rp6<(;nNIN>~gyQ_frImC;l>r863b z{ai?ulT1^~&aorqXll(w-BzB{I_>5x9g{1k)BJ!WrD{B8%i5nbtc&3UmM5GAS27Rj z9gqq!`MS z+jyxoD|L$3#NhDahAF8ioj)lgY#=05wUvNukM{Y4DfEWeF620s&V6QVN-UEJc~!8G ziC43Q;&=H#oKQ!s_OHnd`K^mZfaMM04#q0ye_eDU2va?RI%3zFJDm8aoksgO4KBDz zyREH{Ti0XSj|Y40ey=v*sj)ulx=f|f2W94>p=nZWw?B-wa7CdiqHXE9Nf#Q8_oBvT zw6RGWo6WZyQR8BxfsN6CXDT>f?z<7U22r;kZMW#dezV(t^OJrru63mI(a=ima_Nn2 z($1*Up!XnJl}_hcuIk2SS8LsN zH$FPlM!iwn!gZqjq9Md@lIMH=t!WPZE>kj**0FzoG~9_U9qkLbpk$+1CniPNH|Y{; zyC&k2^7;3~p+HaosliJ@##OBYq)a1c2l}g?}#4 zR{zJlHb?}qOIObCTV<&7eGgxX(5y!}GkPSSNHV3jrY?FgtB$?0lT+3bpAM5EryvX4 z&~a9VhMWIXur+u?pjE?(eUr?;LvVb5!y1j+&ta7GmQOwJ$7zcJ!bQN_Y|O`9273hA$lWeWz=Wow$l Qwd(Av#X}L$kqZ^YKLzI-!vFvP literal 0 HcmV?d00001 diff --git a/registration/locale/pt_BR/LC_MESSAGES/django.po b/registration/locale/pt_BR/LC_MESSAGES/django.po new file mode 100644 index 0000000..9e8addb --- /dev/null +++ b/registration/locale/pt_BR/LC_MESSAGES/django.po @@ -0,0 +1,81 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-19 19:30-0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: forms.py:38 +msgid "username" +msgstr "usuц║rio" + +#: forms.py:41 +msgid "email address" +msgstr "endereц╖o de email" + +#: forms.py:43 +msgid "password" +msgstr "" + +#: forms.py:45 +msgid "password (again)" +msgstr "senha (novamente)" + +#: forms.py:54 +msgid "Usernames can only contain letters, numbers and underscores" +msgstr "Nomes de usuц║rio apenas podem conter letras, nц╨meros, e underscore" + +#: forms.py:59 +msgid "This username is already taken. Please choose another." +msgstr "Este nome de usuц║rio jц║ existe. Por favor, escolha outro." + +#: forms.py:68 +msgid "You must type the same password each time" +msgstr "Vocц╙ deve escrever a mesma senha nos dois campos" + +#: forms.py:96 +msgid "I have read and agree to the Terms of Service" +msgstr "Eu lц╜ e concordo com os Termos de Uso do serviц╖o" + +#: forms.py:105 +msgid "You must agree to the terms to register" +msgstr "Vocц╙ deve concordar com os termos para registrar-se" + +#: forms.py:124 +msgid "" +"This email address is already in use. Please supply a different email " +"address." +msgstr "Este endereц╖o de email jц║ estц║ em uso. Por favor, informe um endereц╖o de email diferente." + +#: forms.py:149 +msgid "" +"Registration using free email addresses is prohibited. Please supply a " +"different email address." +msgstr "Registrar-se com contas de email gratuitos estц║ proibido. Por favor, informe um endereц╖o de email diferente." + +#: models.py:188 +msgid "user" +msgstr "usuц║rio" + +#: models.py:189 +msgid "activation key" +msgstr "chave de ativaц╖цёo" + +#: models.py:194 +msgid "registration profile" +msgstr "profile de registro" + +#: models.py:195 +msgid "registration profiles" +msgstr "profiles de registro" diff --git a/registration/locale/ru/LC_MESSAGES/django.mo b/registration/locale/ru/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..b6eb3f11e78f2d8a75f43fbe480a3d1dd040ab44 GIT binary patch literal 2360 zcmbtTO>Y}j6n#LUU_OOFR1}H3K{Svt4yZJ7OI4G&CBjZ3$B76+La+9VJ=C7DW}ZW{ zKqR41p(I2?Aa#KRV#5mICU%?eh$Xu>OBTRyV9N&19Z!?EO)Eu=G;`kExgY1=d*;ul zk9;d|J%#%P+~47Tj?eeuf$K8R0!{-z1pWjZ0v^3zh&O@bz|+7*;LE_@fL*}9flmOB zKOn^8z*m3|1ABp7D+PQ8m;*iwECG)IKLN7e9PlJ+%>&=V`>*);7x4K9h4=>e53mMY zdPs;*f#RqT9{^^66Tm-!$APDitQ$BAe2l*XUji;hxDw&-z+=7YQKD?Me z$1=YsLreR?eLNbygN6Xr@Is2+3p4~qAwV~rM+}J6LJU$_PAT$~ERu9^mpr9NyQIsC z^2)3FU$7D7y3i08E(Pqin0NllB6>4;nY zy9YWUOCZ`M+LfYJVHuNlMIC7L1y3k^@R-Di=YFIL+8Qic<7f+;PSQxK|J~GuG>xZo zql3dkv7D;8p0=`n$u3%N*Gs;Ycaszw8O~e%*cHpe8jxBg>Gee7l$GeSditoRFZo8o zN}Nn2;ArL4l+DQ_zln4zXJv=2ffT(noXifs`u^}xI+l^Xw(_2I{EF1Bm!$J!nG6l3 zvS~V#&ZY)4=TZX$x%BAhS-3i-x-2OxuVghznbgpQF{XSblQ~zh=in+^4LO>w7RTct5q<+(^M!+&sX}bvHW?fch8TxK)Y7jDY!-GKS{k1c}$pJ z$m|3&W;?iQR)aayFdJl6DVR0eX2oon22L%r1p}Gy;ny(BW|i$`6K>`jKJUPFBUmsu zfy?-?g+xoj{D8O>9A|?$jtjnEbC?IVgM#3{$%U7Z=Q{4D*$U=^&%uw`fg93?2Bw6R z%i$A7Lg_}>MJxD{*_uWiY~gGb&cSQS0tUK zU=c00%z8+D8wt1C9ubpZJ}%79%qW;UR2ZSy4X)cU0vKDUG#f1aKiVMmBbrzTS1w^T zG4I<4&23{OSVY7G?V|R=-75$4*apx0j*+!c@Y=nrKqb5dwAYqYGt7jwa82R~hSC9L zGS@+AombpVp;)e9H}fpUu!klD$}HB=*glng%`Qz&gEG)dyUZ=+f^x9HgAH|efPS0p zSzzn8Yt^g@b4T(}KCH|zF`+Du_&fNIiPR9TcLxpPpW@yKj2qzA#2nWM)`qz$VA}}3 z3a+wvAhLE#+%#7`gbW2nvs0!>wIGT5xFpfL-unY5`lB9$pUYeV-S1cQ>zS RfziJoscS(X`MuO9{sTfWM{WQB literal 0 HcmV?d00001 diff --git a/registration/locale/ru/LC_MESSAGES/django.po b/registration/locale/ru/LC_MESSAGES/django.po new file mode 100644 index 0000000..8b0c309 --- /dev/null +++ b/registration/locale/ru/LC_MESSAGES/django.po @@ -0,0 +1,81 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-19 19:30-0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: forms.py:38 +msgid "username" +msgstr "п╦п╪я▐ п©п╬п╩я▄п╥п╬п╡п╟я┌п╣п╩я▐" + +#: forms.py:41 +msgid "email address" +msgstr "п╟п╢я─п╣я│ я█п╩п╣п╨я┌я─п╬п╫п╫п╬п╧ п©п╬я┤я┌я▀" + +#: forms.py:43 +msgid "password" +msgstr "п©п╟я─п╬п╩я▄" + +#: forms.py:45 +msgid "password (again)" +msgstr "п©п╟я─п╬п╩я▄ (п╡п╣я─п╦я└п╦п╨п╟я├п╦я▐)" + +#: forms.py:54 +msgid "Usernames can only contain letters, numbers and underscores" +msgstr "п≤п╪я▐ п©п╬п╩я▄п╥п╬п╡п╟я┌п╣п╩я▐ п╪п╬п╤п╣я┌ я│п╬п╢п╣я─п╤п╟я┌я▄ я┌п╬п╩я▄п╨п╬ п╠я┐п╨п╡я▀, я├п╦я└я─я▀ п╦ п©п╬п╢я┤п╣я─п╨п╦п╡п╟п╫п╦я▐" + +#: forms.py:59 +msgid "This username is already taken. Please choose another." +msgstr "п╒п╟п╨п╬п╣ п╦п╪я▐ п©п╬п╩я▄п╥п╬п╡п╟я┌п╣п╩я▐ я┐п╤п╣ п╣я│я┌я▄. п÷п╬п╤п╟п╩я┐п╧я│я┌п╟, п╡я▀п╠п╣я─п╦я┌п╣ п╢я─я┐пЁп╬п╣." + +#: forms.py:68 +msgid "You must type the same password each time" +msgstr "п▓я▀ п╢п╬п╩п╤п╫я▀ п╡п╡п╬п╢п╦я┌я▄ п╬п╢п╦п╫ п╦ я┌п╬я┌ п╤п╣ п©п╟я─п╬п╩я▄ п╨п╟п╤п╢я▀п╧ я─п╟п╥" + +#: forms.py:96 +msgid "I have read and agree to the Terms of Service" +msgstr "п╞ п©я─п╬я┤п╦я┌п╟п╩ п╦ я│п╬пЁп╩п╟я│п╣п╫ я│ п÷я─п╟п╡п╦п╩п╟п╪п╦ п≤я│п©п╬п╩я▄п╥п╬п╡п╟п╫п╦я▐" + +#: forms.py:105 +msgid "You must agree to the terms to register" +msgstr "п▓я▀ п╢п╬п╩п╤п╫я▀ я│п╬пЁп╩п╟я│п╦я┌я▄я│я▐ я│ п÷я─п╟п╡п╦п╩п╟п╪п╦ п╢п╩я▐ я─п╣пЁп╦я│я┌я─п╟я├п╦п╦" + +#: forms.py:124 +msgid "" +"This email address is already in use. Please supply a different email " +"address." +msgstr "п╜я┌п╬я┌ п╟п╢я─п╣я│ я█п╩п╣п╨я┌я─п╬п╫п╫п╬п╧ п©п╬я┤я┌я▀ я┐п╤п╣ п╦я│п©п╬п╩я▄п╥я┐п╣я┌я│я▐. п÷п╬п╤п╟п╩я┐п╧я│я┌п╟, п╡п╡п╣п╢п╦я┌п╣ п╢я─я┐пЁп╬п╧ п╟п╢я─п╣я│." + +#: forms.py:149 +msgid "" +"Registration using free email addresses is prohibited. Please supply a " +"different email address." +msgstr "п═п╣пЁп╦я│я┌я─п╟я├п╦я▐ я│ п╦я│п©п╬п╩я▄п╥п╬п╡п╟п╫п╦п╣п╪ я│п╡п╬п╠п╬п╢п╫я▀я┘ п©п╬я┤я┌п╬п╡я▀я┘ я│п╣я─п╡п╣я─п╬п╡ п╥п╟п©я─п╣я┴п╣п╫п╟. п÷п╬п╤п╟п╩я┐п╧я│я┌п╟, п╡п╡п╣п╢п╦я┌п╣ п╢я─я┐пЁп╬п╧ п╟п╢я─п╣я│ я█п╩п╣п╨я┌я─п╬п╫п╫п╬п╧ п©п╬я┤я┌я▀." + +#: models.py:188 +msgid "user" +msgstr "п©п╬п╩я▄п╥п╬п╡п╟я┌п╣п╩я▄" + +#: models.py:189 +msgid "activation key" +msgstr "п╨п╩я▌я┤ п╟п╨я┌п╦п╡п╟я├п╦п╦" + +#: models.py:194 +msgid "registration profile" +msgstr "п©я─п╬я└п╦п╩я▄ я─п╣пЁп╦я│я┌я─п╟я├п╦п╦" + +#: models.py:195 +msgid "registration profiles" +msgstr "п©я─п╬я└п╦п╩п╦ я─п╣пЁп╦я│я┌я─п╟я├п╦п╦" diff --git a/registration/locale/sr/LC_MESSAGES/django.mo b/registration/locale/sr/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..1699326553efe848408441bb04e452d14f341774 GIT binary patch literal 1966 zcmb7^O>Y}T7{>=F6wIqC5USwtP>GVzu30B7ik!L-`huc1ZIn0_(-_#C_gu7dmEC*YspBKR-(7WlzSLc9rn3cd=i zfFai!_%65wz6WOD3GjO`*!>9pgIqs@%lOtW3z34qfj7XDuL#isx4{kYC-55h7kC;x zeNu>1;4=6Icpb!$*aqJQzXe1791Qt?07KqK)A3(Hq_113Kg0<21>ac<^LTyw5n`j% zx`jBihA;wB2$Wsx(Ewk=Bi5-eM@*JwN>by=EHimSzR%QUTRJkkw9R&;5*AyWDd(;9 z%4n*b(i!ardoHCaNT#V}=h%^QG_!UzP-x!o9wZWuqQ|wGV93@hd(Wp5w+Dqgw z*n76_Jn5?Jp;K#C=@i|>VDjRwsi>@+pBEB75E81|T0pkPc78mB-Vob`8i&%k?~F}} zWzr|FN)|HlYBW)Nz+*9|j+ogWb!b6mD0ERwGEJ8Xi?OQ;e&$anf#H8yW|qbpdouu-C`(sP@Z z;&>&BFGTT0YAv@fw$8=)1H)*GM=GRwhTVmB9G{C@u)8UpkGfWBS4eMcn{IK>o6ahkanxm5qOOhgjcdo#YBl4=6|5|FV${Vtw8`^* z|JksBf~%DDrFHB(JKgKi%CTLj0lPQ4rW2Fm1hi=d+q|(^R8|(zb%T{|lOEQeuCu&K zlWs?U+=@G$7M(pyp&NhHX|?DJiV4>EzT8=w4PEM72;GI+>{2VH2M;DLu!$Gp(}K^^ zYU7(|({QSMG@E5R9MqnY`grJyskFD_Hr?I%`u6tCwL9x;jj+TA`^rlbHXSK~riXuU zN#Tr2oUy`;R4Hj-tU7#r@Q`&bkB)w#^0^VX_1Hl&uT@4IO>Y961xY#6p6o%eoV9Gy zhQW4Zsx*5=sjaeV&c14j+ao#ms}3>ZgNIa6H8kFGPaVYx`l}lR`2xlIN)x4hxFLd<-M~4j(Jrvtdw&a9FKM$U(Rl y5*!>wgCegE_K)s{Aik)5W}t, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: django-registration trunk\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2008-04-05 13:51+0200\n" +"PO-Revision-Date: 2008-04-05 14:00+0100\n" +"Last-Translator: Nebojsa Djordjevic \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Poedit-Language: Serbian\n" +"X-Poedit-Country: YUGOSLAVIA\n" + +#: forms.py:38 +msgid "username" +msgstr "korisniд█ko ime" + +#: forms.py:41 +msgid "email address" +msgstr "email adresa" + +#: forms.py:43 +msgid "password" +msgstr "е║ifra" + +#: forms.py:45 +msgid "password (again)" +msgstr "е║ifra (ponovo)" + +#: forms.py:54 +msgid "Usernames can only contain letters, numbers and underscores" +msgstr "Korisniд█ko ime moе╬e da se sastoji samo od slova, brojeva i donje crte (\"_\")" + +#: forms.py:59 +msgid "This username is already taken. Please choose another." +msgstr "Korisniд█ko ime je veд┤ zauzeto. Izaberite drugo." + +#: forms.py:71 +msgid "You must type the same password each time" +msgstr "Unete е║ifre se ne slaе╬u" + +#: forms.py:100 +msgid "I have read and agree to the Terms of Service" +msgstr "Proд█itao sam i slaе╬em se sa uslovima koriе║д┤enja" + +#: forms.py:109 +msgid "You must agree to the terms to register" +msgstr "Morate se sloе╬iti sa uslovima koriе║д┤enja da bi ste se registrovali" + +#: forms.py:128 +msgid "This email address is already in use. Please supply a different email address." +msgstr "Ova e-mail adresa je veд┤ u upotrebi. Morate koristiti drugu e-mail adresu." + +#: forms.py:153 +msgid "Registration using free email addresses is prohibited. Please supply a different email address." +msgstr "Registracija koriе║д┤enjem besplatnig e-mail adresa je zabranjena. Morate uneti drugu e-mail adresu." + +#: models.py:188 +msgid "user" +msgstr "korisnik" + +#: models.py:189 +msgid "activation key" +msgstr "aktivacioni kljuд█" + +#: models.py:194 +msgid "registration profile" +msgstr "registracioni profil" + +#: models.py:195 +msgid "registration profiles" +msgstr "registracioni profili" + diff --git a/registration/locale/sv/LC_MESSAGES/django.mo b/registration/locale/sv/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..50eca67e21301f5081b881b054ec648a67e25509 GIT binary patch literal 1687 zcmb7^&2A$_5XT!>STLWbWfg3^`kvMRS#T(=W=EO_Dl`C(+zsF7-qrEW7%&%u$RbBnBs{Hx-jb8+=*Kog! z`#0Qg`}I?J;Cc)O;1v8C{2e?5Z$2%=C*Ub~7yKD~AN&Jc1OEYE0pEE>h?l_+z~{j# z==Z9FZ-QO$EieXefZu`M?g#Kw?DZ4)9^P~OxC8zT-T~L26=Dlya3A~?+y?&w34G%@ zA#Q;mfiHpk;0xd}==c8$#Gg=j`2EIU3H%=P`ThuE`{MHNK8DZl)#A(i;p4oxc!#+_ zs>S8wU0ye_fD{606?*tRd>*1n136(bEF+Q%cWju+X>tRm9-GXP?$a@wNfNT?a-3La zq)W7-u}xG=eQ(d1Oj43jWY}7^lvo-WJxI3eCnU!ZOr2ym+X`l#Q)h6%sxW5~`X)KsHM|HyuH5h;4n2BWdlo z+C;=M9FR*g7BY0nWUly-r(#7NacO_GpxZJ=qHAKl)1p);Mn6gUi65=-3Kb78lQ=T^ z8xCF2jDk}f3ze?XQGNHz`ay$E8{Ok(=dje}kv1-9+c=4WopEe~o~}{psM8B}Q7hjb z!Jc$nqpeD%8dSD}t!>(@);_+oUfHZvU>J0HlK9{YyN8W>H)wZ)y*ho~skNKyk2;5q zQcGGF^o&$Cl}?))H8S+sv15fU%r7#!$Jv9S8a=QBfR(nw^hc#zN1RMMFqT9=j|U)6b{E84ibH z4?~g9EKYc($)zL+dI5c~<)UMu7yd!SVke(P5^7#pR(>KD0VaO?G#N@!A7U`rpafP; k!y%`laZOgyy7XMG=yn2y)_jX&92P+DU!9VdORtak7w$#!*8l(j literal 0 HcmV?d00001 diff --git a/registration/locale/sv/LC_MESSAGES/django.po b/registration/locale/sv/LC_MESSAGES/django.po new file mode 100644 index 0000000..dec76e2 --- /dev/null +++ b/registration/locale/sv/LC_MESSAGES/django.po @@ -0,0 +1,81 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2008-03-23 18:59+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: Emil Stenstrц╤m \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: .\forms.py:38 +msgid "username" +msgstr "Anvц╓ndarnamn" + +#: .\forms.py:41 +msgid "email address" +msgstr "E-postadress" + +#: .\forms.py:43 +msgid "password" +msgstr "Lц╤senord" + +#: .\forms.py:45 +msgid "password (again)" +msgstr "Lц╤senord (igen)" + +#: .\forms.py:54 +msgid "Usernames can only contain letters, numbers and underscores" +msgstr "Anvц╓ndarnamn fц╔r bara innehц╔lla bokstц╓ver, siffror och understreck" + +#: .\forms.py:59 +msgid "This username is already taken. Please choose another." +msgstr "Det anvц╓ndarnamnet ц╓r upptaget. Prova ett annat." + +#: .\forms.py:71 +msgid "You must type the same password each time" +msgstr "Bц╔da lц╤senord mц╔ste vara lika" + +#: .\forms.py:100 +msgid "I have read and agree to the Terms of Service" +msgstr "Jag har lц╓st och accepterar avtalet" + +#: .\forms.py:109 +msgid "You must agree to the terms to register" +msgstr "Du mц╔ste acceptera avtalet fц╤r att registrera dig" + +#: .\forms.py:128 +msgid "" +"This email address is already in use. Please supply a different email " +"address." +msgstr "Den e-postadressen ц╓r upptagen, anvц╓nd an annan adress." + +#: .\forms.py:153 +msgid "" +"Registration using free email addresses is prohibited. Please supply a " +"different email address." +msgstr "Gratis e-postadresser ц╓r inte tillц╔tna, anvц╓nd en annan adress." + +#: .\models.py:188 +msgid "user" +msgstr "Anvц╓ndare" + +#: .\models.py:189 +msgid "activation key" +msgstr "Aktiveringsnyckel" + +#: .\models.py:194 +msgid "registration profile" +msgstr "Profil" + +#: .\models.py:195 +msgid "registration profiles" +msgstr "Profiler" diff --git a/registration/locale/zh_CN/LC_MESSAGES/django.mo b/registration/locale/zh_CN/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..ece5cc97b10d0ace9a2ba33ce927e95d8e48ec49 GIT binary patch literal 1669 zcmb7@?@t^>7{{ktt)4&AXnav$%nPGdyXOK?;iwIWNKl}J9whq4O!tPpwRgLhojEKq zCI=P-%LPQMMri^KRMOg9qeLiBc;yTK3E%W)x$_P2Kkz%Z2iHq{)5-39W@evxetc*4 z@8d_W5VU9UzKC}L?+bc-1RdH%&;UolPrzTmKJeH>guDd~fvsR3dCj!2cH86z~@05JPLjRYP)f;1+gxJFJb;CKK6tEfFFRZkA!(mf+pr` z;0aKH+WyF+gggnpTxAo8hqR$P4t7=7Kd#aS@eqmbaqx2x;VVt&uYDmjd8V=wcDjxy zs&l;;saBfK|DYYi08$X7Rna2?YX790CfNw5K4%HaT)ei=Ih7ujNlxS3PYdb|({tP( zaV$;-xa|n(GwFCP%?ii0>9DrvY38ITOC)?Q1Q*m1G~;_o=e#3%B1-#HoC!`vHj_z> zQbrTb@G$qeE9*B#Nj!<2bMEfeYYy-&F%jqZdogtBnbG1}kW@nL&cCmre>IM;o)FEYS0 zo-d7FVLJ)q-K;H)xEG_5{)%3X~ccziWHNcAEU{v%tD_uJ$*20v-RDJT3-4L z%w0RnY;MFkOUGzWdtcX}PWyCEPg}}~dcJ)o(%aqJSu3m|YDPM6wz!K%948T@l3$c> zWKz(um0C&W3ohRojK6O**X(pYxW7iHYk3L$12Ni+iyk4t)HiDVxAKn@rNWlF^M{(7 z2`RRsQpl%H*1Pjx}&CM{z1G++1!PL5% z%fn6S+D(;T#7gJ`DU@^5VLCc+&2;ug>C3xnWpCBgWr+MOsbZH3B&rB-D*{8rleR!yuFH, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-19 19:30-0500\n" +"PO-Revision-Date: 2008-03-20 23:22+0800\n" +"Last-Translator: hutuworm \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: forms.py:38 +msgid "username" +msgstr "Г■╗Ф┬╥Е░█" + +#: forms.py:41 +msgid "email address" +msgstr "Email Е°╟Е²─" + +#: forms.py:43 +msgid "password" +msgstr "Е╞├Г═│" + +#: forms.py:45 +msgid "password (again)" +msgstr "Е╞├Г═│О╪┬И┤█Е╓█О╪┴" + +#: forms.py:54 +msgid "Usernames can only contain letters, numbers and underscores" +msgstr "Г■╗Ф┬╥Е░█Е▐╙Х┐╫Е▄┘Е░╚Е╜≈Ф╞█Ц─│Ф∙╟Е╜≈Е▓▄Д╦▀Е┬▓Г╨©" + +#: forms.py:59 +msgid "This username is already taken. Please choose another." +msgstr "Х╞╔Г■╗Ф┬╥Е░█Е╥╡Х╒╚Е█═Г■╗О╪▄Х╞╥Е▐╕И─┴Д╦─Д╦╙Ц─┌" + +#: forms.py:68 +msgid "You must type the same password each time" +msgstr "Ф┌╗Е©┘И║╩Х╬⌠Е┘╔Д╦╓И│█Е░▄Ф═╥Г └Е╞├Г═│" + +#: forms.py:96 +msgid "I have read and agree to the Terms of Service" +msgstr "Ф┬▒Е╥╡И≤┘Х╞╩Е╧╤Е░▄Ф└▐Х╞╔Ф°█Е┼║Ф²║Ф╛╬" + +#: forms.py:105 +msgid "You must agree to the terms to register" +msgstr "Ф┌╗Е©┘И║╩Е░▄Ф└▐ФЁ╗Е├▄Ф²║Ф╛╬" + +#: forms.py:124 +msgid "This email address is already in use. Please supply a different email address." +msgstr "Х╞╔ Email Е°╟Е²─Е╥╡Ф°┴Д╨╨Д╫©Г■╗О╪▄Х╞╥Ф▐░Д╬⌡Д╦─Д╦╙Е▐╕Е╓√Г └ Email Е°╟Е²─Ц─┌" + +#: forms.py:149 +msgid "Registration using free email addresses is prohibited. Please supply a different email address." +msgstr "Г╕│Ф╜╒Д╫©Г■╗Е┘█Х╢╧ Email Е°╟Е²─ФЁ╗Е├▄О╪▄Х╞╥Ф▐░Д╬⌡Д╦─Д╦╙Е▐╕Е╓√Г └ Email Е°╟Е²─Ц─┌" + +#: models.py:188 +msgid "user" +msgstr "Г■╗Ф┬╥" + +#: models.py:189 +msgid "activation key" +msgstr "Ф©─Ф╢╩Е╞├И▓╔" + +#: models.py:194 +msgid "registration profile" +msgstr "ФЁ╗Е├▄Д©║Ф│╞" + +#: models.py:195 +msgid "registration profiles" +msgstr "ФЁ╗Е├▄Д©║Ф│╞" + diff --git a/registration/locale/zh_TW/LC_MESSAGES/django.mo b/registration/locale/zh_TW/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..24a3534958925d49ab1e1032f3cd2d50e28cd3e8 GIT binary patch literal 1669 zcmb7@?@t^>7{{ktt)4&AXnav$%nPGd-E)DcaMT7wBq&fq4-$Q2rhCKQ+PmG$&K#B) zlYz5_j$aFc%Reb1L)AsfCe}YehmHu4uFRqAmmMO1Z)TM;7j0N;8E~j@JaB< zgM>T*z5+f1wt_lVC-@9F1U?Je;6d!XO5#cEY;dMsv6N14oH{xtzh8IQ;=3%(@AV|vY3}YjvXY*M^qA|R zXg^AZYdZwBm`gnuIa{798RDn7l-w7uQ8$}Dg+W!btee2l@=!ea(96(#nu?Ipz zRlh1A_v?0YJOjNUw$M3dm=K?Oeu8plC8>1MoG?o|W1-@w`8a7%hwRz!59rIxMxsYa z7^!wtIE+yz#s7Cv5Uo(P(=w4k-}{7H(&$YXBPa_e9ixMtT_1GzbkmXUp_9EQ1|mZ| z$H#c^>w72sOQ_qBmKSo-SxtnqGqHEXN$XN#BmZaD)|}t zdL{)8+o_dgzTooh;rM$-Yu!%ggZpcAyOx*0KM

    xabiQ%v@BrHY!(sEB~4+6_(Y) zTySo_yt)xA=GE-i!P2$hmo2ihJP}-9Ds67>Tv}06`SQjc+Fkv7)Zz_wEf@U0qNXqG z7U#-qv%&mAY3rL(A*XV)YJOS$aAoJ}xd!XIxk-Wm4QogtSUg|4eLIYCpRSNqF!P66 z$iq!}X|1$*2`ixwq)?eCgz4zOb<_Eu%V#&$+>}~aQ`f%=*7J9B6T#&I#_G$tQejqI zIJ>6+nG?Au+*9?``^mNf|~kKO|4cY^4f0YdlaqKr+1c$B-qXcH}7Cl pxp;%zs~V{pgxD?4RKCcot<~M)c@jn`ZGRU`tT#@={Oe_q{{ZHmUoijx literal 0 HcmV?d00001 diff --git a/registration/locale/zh_TW/LC_MESSAGES/django.po b/registration/locale/zh_TW/LC_MESSAGES/django.po new file mode 100644 index 0000000..7cc090d --- /dev/null +++ b/registration/locale/zh_TW/LC_MESSAGES/django.po @@ -0,0 +1,77 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2007-09-19 19:30-0500\n" +"PO-Revision-Date: 2008-03-20 23:22+0800\n" +"Last-Translator: hutuworm \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: forms.py:38 +msgid "username" +msgstr "Г■╗Ф┬╤Е░█" + +#: forms.py:41 +msgid "email address" +msgstr "Email Е°╟Е²─" + +#: forms.py:43 +msgid "password" +msgstr "Е╞├Г╒╪" + +#: forms.py:45 +msgid "password (again)" +msgstr "Е╞├Г╒╪О╪┬И┤█Е╬╘О╪┴" + +#: forms.py:54 +msgid "Usernames can only contain letters, numbers and underscores" +msgstr "Г■╗Ф┬╤Е░█Е▐╙Х┐╫Е▄┘Е░╚Е╜≈Ф╞█Ц─│Ф∙╦Е╜≈Е▓▄Д╦▀Е┼┐Г╥ " + +#: forms.py:59 +msgid "This username is already taken. Please choose another." +msgstr "Х╘╡Г■╗Ф┬╤Е░█Е╥╡Х╒╚Д╫■Г■╗О╪▄Х╚▀Е▐╕И│╦Д╦─Е─▀Ц─┌" + +#: forms.py:68 +msgid "You must type the same password each time" +msgstr "Ф┌╗Е©┘И═┬Х╪╦Е┘╔Е┘╘И│█Е░▄Ф╗ёГ └Е╞├Г╒╪" + +#: forms.py:96 +msgid "I have read and agree to the Terms of Service" +msgstr "Ф┬▒Е╥╡И√╠Х╝─Д╦╕Е░▄Ф└▐Х╘╡Ф°█Е▀≥Ф╒²Ф╛╬" + +#: forms.py:105 +msgid "You must agree to the terms to register" +msgstr "Ф┌╗Е©┘И═┬Е░▄Ф└▐ФЁ╗Е├┼Ф╒²Ф╛╬" + +#: forms.py:124 +msgid "This email address is already in use. Please supply a different email address." +msgstr "Х╘╡ Email Е°╟Е²─Е╥╡Ф°┴Д╨╨Д╫©Г■╗О╪▄Х╚▀Ф▐░Д╬⌡Д╦─Е─▀Е▐╕Е╓√Г └ Email Е°╟Е²─Ц─┌" + +#: forms.py:149 +msgid "Registration using free email addresses is prohibited. Please supply a different email address." +msgstr "Г╕│Ф╜╒Д╫©Г■╗Е┘█Х╡╩ Email Е°╟Е²─ФЁ╗Е├┼О╪▄Х╚▀Ф▐░Д╬⌡Д╦─Е─▀Е▐╕Е╓√Г └ Email Е°╟Е²─Ц─┌" + +#: models.py:188 +msgid "user" +msgstr "Г■╗Ф┬╤" + +#: models.py:189 +msgid "activation key" +msgstr "Ф©─Ф╢╩Е╞├И▒╟" + +#: models.py:194 +msgid "registration profile" +msgstr "ФЁ╗Е├┼Д©║Ф│╞" + +#: models.py:195 +msgid "registration profiles" +msgstr "ФЁ╗Е├┼Д©║Ф│╞" + diff --git a/registration/management/__init__.py b/registration/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/registration/management/commands/__init__.py b/registration/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/registration/management/commands/cleanupregistration.py b/registration/management/commands/cleanupregistration.py new file mode 100644 index 0000000..cfaee32 --- /dev/null +++ b/registration/management/commands/cleanupregistration.py @@ -0,0 +1,20 @@ +""" +A management command which deletes expired accounts (e.g., +accounts which signed up but never activated) from the database. + +Calls ``RegistrationProfile.objects.delete_expired_users()``, which +contains the actual logic for determining which accounts are deleted. + +""" + +from django.core.management.base import NoArgsCommand +from django.core.management.base import CommandError + +from registration.models import RegistrationProfile + + +class Command(NoArgsCommand): + help = "Delete expired user registrations from the database" + + def handle_noargs(self, **options): + RegistrationProfile.objects.delete_expired_users() diff --git a/registration/models.py b/registration/models.py new file mode 100644 index 0000000..04980af --- /dev/null +++ b/registration/models.py @@ -0,0 +1,231 @@ +import datetime +import random +import re +try: + from hashlib import sha +except ImportError: + import sha as shalib + def sha(val): return shalib.new(val) + +from django.conf import settings +from django.db import models +from django.template.loader import render_to_string +from django.utils.translation import ugettext_lazy as _ +from django.contrib.auth.models import User +from django.contrib.sites.models import Site + + +SHA1_RE = re.compile('^[a-f0-9]{40}$') + + +class RegistrationManager(models.Manager): + """ + Custom manager for the ``RegistrationProfile`` model. + + The methods defined here provide shortcuts for account creation + and activation (including generation and emailing of activation + keys), and for cleaning out expired inactive accounts. + + """ + def activate_user(self, activation_key): + """ + Validate an activation key and activate the corresponding + ``User`` if valid. + + If the key is valid and has not expired, return the ``User`` + after activating. + + If the key is not valid or has expired, return ``False``. + + If the key is valid but the ``User`` is already active, + return ``False``. + + To prevent reactivation of an account which has been + deactivated by site administrators, the activation key is + reset to the string ``ALREADY_ACTIVATED`` after successful + activation. + + """ + # Make sure the key we're trying conforms to the pattern of a + # SHA1 hash; if it doesn't, no point trying to look it up in + # the database. + if SHA1_RE.search(activation_key): + try: + profile = self.get(activation_key=activation_key) + except self.model.DoesNotExist: + return False + if not profile.activation_key_expired(): + user = profile.user + user.is_active = True + user.save() + profile.activation_key = "ALREADY_ACTIVATED" + profile.save() + return user + return False + + def create_inactive_user(self, username, password, email, + send_email=True, profile_callback=None): + """ + Create a new, inactive ``User``, generates a + ``RegistrationProfile`` and email its activation key to the + ``User``, returning the new ``User``. + + To disable the email, call with ``send_email=False``. + + To enable creation of a custom user profile along with the + ``User`` (e.g., the model specified in the + ``AUTH_PROFILE_MODULE`` setting), define a function which + knows how to create and save an instance of that model with + appropriate default values, and pass it as the keyword + argument ``profile_callback``. This function should accept one + keyword argument: + + ``user`` + The ``User`` to relate the profile to. + + """ + new_user = User.objects.create_user(username, email, password) + new_user.is_active = False + new_user.save() + + registration_profile = self.create_profile(new_user) + + if profile_callback is not None: + profile_callback(user=new_user) + + if send_email: + from django.core.mail import send_mail + current_site = Site.objects.get_current() + + subject = render_to_string('registration/activation_email_subject.txt', + { 'site': current_site }) + # Email subject *must not* contain newlines + subject = ''.join(subject.splitlines()) + + message = render_to_string('registration/activation_email.txt', + { 'activation_key': registration_profile.activation_key, + 'expiration_days': settings.ACCOUNT_ACTIVATION_DAYS, + 'site': current_site }) + + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [new_user.email]) + return new_user + + def create_profile(self, user): + """ + Create a ``RegistrationProfile`` for a given + ``User``, and return the ``RegistrationProfile``. + + The activation key for the ``RegistrationProfile`` will be a + SHA1 hash, generated from a combination of the ``User``'s + username and a random salt. + + """ + salt = sha(str(random.random())).hexdigest()[:5] + activation_key = sha(salt+user.username).hexdigest() + return self.create(user=user, + activation_key=activation_key) + + def delete_expired_users(self): + """ + Remove expired instances of ``RegistrationProfile`` and their + associated ``User``s. + + Accounts to be deleted are identified by searching for + instances of ``RegistrationProfile`` with expired activation + keys, and then checking to see if their associated ``User`` + instances have the field ``is_active`` set to ``False``; any + ``User`` who is both inactive and has an expired activation + key will be deleted. + + It is recommended that this method be executed regularly as + part of your routine site maintenance; this application + provides a custom management command which will call this + method, accessible as ``manage.py cleanupregistration``. + + Regularly clearing out accounts which have never been + activated serves two useful purposes: + + 1. It alleviates the ocasional need to reset a + ``RegistrationProfile`` and/or re-send an activation email + when a user does not receive or does not act upon the + initial activation email; since the account will be + deleted, the user will be able to simply re-register and + receive a new activation key. + + 2. It prevents the possibility of a malicious user registering + one or more accounts and never activating them (thus + denying the use of those usernames to anyone else); since + those accounts will be deleted, the usernames will become + available for use again. + + If you have a troublesome ``User`` and wish to disable their + account while keeping it in the database, simply delete the + associated ``RegistrationProfile``; an inactive ``User`` which + does not have an associated ``RegistrationProfile`` will not + be deleted. + + """ + for profile in self.all(): + if profile.activation_key_expired(): + user = profile.user + if not user.is_active: + user.delete() + + +class RegistrationProfile(models.Model): + """ + A simple profile which stores an activation key for use during + user account registration. + + Generally, you will not want to interact directly with instances + of this model; the provided manager includes methods + for creating and activating new accounts, as well as for cleaning + out accounts which have never been activated. + + While it is possible to use this model as the value of the + ``AUTH_PROFILE_MODULE`` setting, it's not recommended that you do + so. This model's sole purpose is to store data temporarily during + account registration and activation, and a mechanism for + automatically creating an instance of a site-specific profile + model is provided via the ``create_inactive_user`` on + ``RegistrationManager``. + + """ + user = models.ForeignKey(User, unique=True, verbose_name=_('user')) + activation_key = models.CharField(_('activation key'), max_length=40) + + objects = RegistrationManager() + + class Meta: + verbose_name = _('registration profile') + verbose_name_plural = _('registration profiles') + + def __unicode__(self): + return u"Registration information for %s" % self.user + + def activation_key_expired(self): + """ + Determine whether this ``RegistrationProfile``'s activation + key has expired, returning a boolean -- ``True`` if the key + has expired. + + Key expiration is determined by a two-step process: + + 1. If the user has already activated, the key will have been + reset to the string ``ALREADY_ACTIVATED``. Re-activating is + not permitted, and so this method returns ``True`` in this + case. + + 2. Otherwise, the date the user signed up is incremented by + the number of days specified in the setting + ``ACCOUNT_ACTIVATION_DAYS`` (which should be the number of + days after signup during which a user is allowed to + activate their account); if the result is less than or + equal to the current date, the key has expired and this + method returns ``True``. + + """ + expiration_date = datetime.timedelta(days=settings.ACCOUNT_ACTIVATION_DAYS) + return self.activation_key == "ALREADY_ACTIVATED" or \ + (self.user.date_joined + expiration_date <= datetime.datetime.now()) + activation_key_expired.boolean = True diff --git a/registration/urls.py b/registration/urls.py new file mode 100644 index 0000000..7a46224 --- /dev/null +++ b/registration/urls.py @@ -0,0 +1,60 @@ +""" +URLConf for Django user registration and authentication. + +Recommended usage is a call to ``include()`` in your project's root +URLConf to include this URLConf for any URL beginning with +``/accounts/``. + +""" + + +from django.conf.urls.defaults import * +from django.views.generic.simple import direct_to_template +from django.contrib.auth import views as auth_views + +from registration.views import activate +from registration.views import register + + +urlpatterns = patterns('', + # Activation keys get matched by \w+ instead of the more specific + # [a-fA-F0-9]{40} because a bad activation key should still get to the view; + # that way it can return a sensible "invalid key" message instead of a + # confusing 404. + url(r'^activate/(?P\w+)/$', + activate, + name='registration_activate'), + url(r'^login/$', + auth_views.login, + {'template_name': 'registration/login.html'}, + name='auth_login'), + url(r'^logout/$', + auth_views.logout, + {'template_name': 'registration/logout.html'}, + name='auth_logout'), + url(r'^password/change/$', + auth_views.password_change, + name='auth_password_change'), + url(r'^password/change/done/$', + auth_views.password_change_done, + name='auth_password_change_done'), + url(r'^password/reset/$', + auth_views.password_reset, + name='auth_password_reset'), + url(r'^password/reset/confirm/(?P[0-9A-Za-z]+)-(?P.+)/$', + auth_views.password_reset_confirm, + name='auth_password_reset_confirm'), + url(r'^password/reset/complete/$', + auth_views.password_reset_complete, + name='auth_password_reset_complete'), + url(r'^password/reset/done/$', + auth_views.password_reset_done, + name='auth_password_reset_done'), + url(r'^register/$', + register, + name='registration_register'), + url(r'^register/complete/$', + direct_to_template, + {'template': 'registration/registration_complete.html'}, + name='registration_complete'), + ) diff --git a/registration/views.py b/registration/views.py new file mode 100644 index 0000000..7f1fafe --- /dev/null +++ b/registration/views.py @@ -0,0 +1,164 @@ +""" +Views which allow users to create and activate accounts. + +""" + + +from django.conf import settings +from django.core.urlresolvers import reverse +from django.http import HttpResponseRedirect +from django.shortcuts import render_to_response +from django.template import RequestContext + +from registration.forms import RegistrationForm +from registration.models import RegistrationProfile + + +def activate(request, activation_key, + template_name='registration/activate.html', + extra_context=None): + """ + Activate a ``User``'s account from an activation key, if their key + is valid and hasn't expired. + + By default, use the template ``registration/activate.html``; to + change this, pass the name of a template as the keyword argument + ``template_name``. + + **Required arguments** + + ``activation_key`` + The activation key to validate and use for activating the + ``User``. + + **Optional arguments** + + ``extra_context`` + A dictionary of variables to add to the template context. Any + callable object in this dictionary will be called to produce + the end result which appears in the context. + + ``template_name`` + A custom template to use. + + **Context:** + + ``account`` + The ``User`` object corresponding to the account, if the + activation was successful. ``False`` if the activation was not + successful. + + ``expiration_days`` + The number of days for which activation keys stay valid after + registration. + + Any extra variables supplied in the ``extra_context`` argument + (see above). + + **Template:** + + registration/activate.html or ``template_name`` keyword argument. + + """ + activation_key = activation_key.lower() # Normalize before trying anything with it. + account = RegistrationProfile.objects.activate_user(activation_key) + if extra_context is None: + extra_context = {} + context = RequestContext(request) + for key, value in extra_context.items(): + context[key] = callable(value) and value() or value + return render_to_response(template_name, + { 'account': account, + 'expiration_days': settings.ACCOUNT_ACTIVATION_DAYS }, + context_instance=context) + + +def register(request, success_url=None, + form_class=RegistrationForm, profile_callback=None, + template_name='registration/registration_form.html', + extra_context=None): + """ + Allow a new user to register an account. + + Following successful registration, issue a redirect; by default, + this will be whatever URL corresponds to the named URL pattern + ``registration_complete``, which will be + ``/accounts/register/complete/`` if using the included URLConf. To + change this, point that named pattern at another URL, or pass your + preferred URL as the keyword argument ``success_url``. + + By default, ``registration.forms.RegistrationForm`` will be used + as the registration form; to change this, pass a different form + class as the ``form_class`` keyword argument. The form class you + specify must have a method ``save`` which will create and return + the new ``User``, and that method must accept the keyword argument + ``profile_callback`` (see below). + + To enable creation of a site-specific user profile object for the + new user, pass a function which will create the profile object as + the keyword argument ``profile_callback``. See + ``RegistrationManager.create_inactive_user`` in the file + ``models.py`` for details on how to write this function. + + By default, use the template + ``registration/registration_form.html``; to change this, pass the + name of a template as the keyword argument ``template_name``. + + **Required arguments** + + None. + + **Optional arguments** + + ``form_class`` + The form class to use for registration. + + ``extra_context`` + A dictionary of variables to add to the template context. Any + callable object in this dictionary will be called to produce + the end result which appears in the context. + + ``profile_callback`` + A function which will be used to create a site-specific + profile instance for the new ``User``. + + ``success_url`` + The URL to redirect to on successful registration. + + ``template_name`` + A custom template to use. + + **Context:** + + ``form`` + The registration form. + + Any extra variables supplied in the ``extra_context`` argument + (see above). + + **Template:** + + registration/registration_form.html or ``template_name`` keyword + argument. + + """ + if request.method == 'POST': + form = form_class(data=request.POST, files=request.FILES) + if form.is_valid(): + new_user = form.save(profile_callback=profile_callback) + # success_url needs to be dynamically generated here; setting a + # a default value using reverse() will cause circular-import + # problems with the default URLConf for this application, which + # imports this file. + return HttpResponseRedirect(success_url or reverse('registration_complete')) + else: + form = form_class() + + if extra_context is None: + extra_context = {} + context = RequestContext(request) + for key, value in extra_context.items(): + context[key] = callable(value) and value() or value + return render_to_response(template_name, + { 'form': form }, + context_instance=context) From 2dcc7ddfcab56ecace74bc0b3d766789071b63ce Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Thu, 8 Apr 2010 10:00:31 +0100 Subject: [PATCH 109/116] Moved exec script to cron infrastructure --- registration/bin/delete_expired_users.py | 19 ------------------- registration/cron.py | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 19 deletions(-) delete mode 100644 registration/bin/delete_expired_users.py create mode 100644 registration/cron.py diff --git a/registration/bin/delete_expired_users.py b/registration/bin/delete_expired_users.py deleted file mode 100644 index 87bd97f..0000000 --- a/registration/bin/delete_expired_users.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -A script which removes expired/inactive user accounts from the -database. - -This is intended to be run as a cron job; for example, to have it run -at midnight each Sunday, you could add lines like the following to -your crontab:: - - DJANGO_SETTINGS_MODULE=yoursite.settings - 0 0 * * sun python /path/to/registration/bin/delete_expired_users.py - -See the method ``delete_expired_users`` of the ``RegistrationManager`` -class in ``registration/models.py`` for further documentation. - -""" - -if __name__ == '__main__': - from registration.models import RegistrationProfile - RegistrationProfile.objects.delete_expired_users() diff --git a/registration/cron.py b/registration/cron.py new file mode 100644 index 0000000..79a1187 --- /dev/null +++ b/registration/cron.py @@ -0,0 +1,20 @@ +import logging + +from registration.models import RegistrationProfile + +class RemoveExpiredProfiles(): + """ + Deletes expired profile requests + """ + + # run every 2 hours + run_every = 7200 + + @property + def _logger(self): + if not hasattr(self, '__logger'): + self.__logger = logging.getLogger(__name__) + return self.__logger + + def job(self): + RegistrationProfile.objects.delete_expired_users() From 975b20b5d40aa605d183b91ca94de34d25ce296a Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Thu, 8 Apr 2010 10:00:56 +0100 Subject: [PATCH 110/116] Removed registration from the init script --- init.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/init.sh b/init.sh index 0836fc1..5c21d7e 100755 --- a/init.sh +++ b/init.sh @@ -1,5 +1,4 @@ #!/bin/sh -svn checkout http://django-registration.googlecode.com/svn/trunk/registration svn co http://code.djangoproject.com/svn/django/branches/releases/1.1.X/django django svn co http://django-evolution.googlecode.com/svn/trunk/django_evolution From 1fdfbb72a22375595241b6516eb3ff17b00abc79 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Thu, 8 Apr 2010 10:04:26 +0100 Subject: [PATCH 111/116] Added registration expiries to the cronjobs --- cronjobs.txt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cronjobs.txt b/cronjobs.txt index 9fc9cec..2ac7cbd 100644 --- a/cronjobs.txt +++ b/cronjobs.txt @@ -1,6 +1,7 @@ ROOT=/home/matalok/auth/auth -@daily $ROOT/run-cron.py reddit.cron UpdateAPIs > $ROOT/logs/redditapi-update.log 2>&1 -*/10 * * * * $ROOT/run-cron.py reddit.cron ProcessValidations > $ROOT/logs/reddit-validations.log 2>&1 -*/5 * * * * $ROOT/run-cron.py eve_api.cron UpdateAPIs > $ROOT/logs/eveapi-update.log 2>&1 -@hourly $ROOT/run-cron.py sso.cron RemoveInvalidUsers > $ROOT/logs/auth-update.log 2>&1 +@daily $ROOT/run-cron.py reddit.cron UpdateAPIs > $ROOT/logs/redditapi-update.log 2>&1 +*/10 * * * * $ROOT/run-cron.py reddit.cron ProcessValidations > $ROOT/logs/reddit-validations.log 2>&1 +*/5 * * * * $ROOT/run-cron.py eve_api.cron UpdateAPIs > $ROOT/logs/eveapi-update.log 2>&1 +@hourly $ROOT/run-cron.py sso.cron RemoveInvalidUsers > $ROOT/logs/auth-update.log 2>&1 +@daily $ROOT/run-cron.py registration.cron RemoveExpiredProfiles > /dev/null 2>&1 From bd3fa3a2666b929aa6a34bebf656e515caec2a32 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Thu, 8 Apr 2010 18:55:22 +0100 Subject: [PATCH 112/116] Fix typo in variable name, doh! --- sso/services/wiki/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sso/services/wiki/__init__.py b/sso/services/wiki/__init__.py index 43918a9..d82dee6 100644 --- a/sso/services/wiki/__init__.py +++ b/sso/services/wiki/__init__.py @@ -102,7 +102,7 @@ class MediawikiService(BaseService): pwhash = self._gen_mw_hash(password) self._dbcursor.execute(self.SQL_ENABLE_USER, [pwhash, uid]) self._db.connection.commit() - self._dbcursor.execute(self.SQL_ENABBLE_GROUP, [uid]) + self._dbcursor.execute(self.SQL_ENABLE_GROUP, [uid]) self._db.connection.commit() return True From f82e211d9140530ed9ec18b3ea801e7043d7fd6c Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 9 Apr 2010 12:46:10 +0100 Subject: [PATCH 113/116] Jabber Service API now uses XMLRPC calls to add/change users Old service access methods have been removed, XMLRPC is the prefered choice due to speed and reliability of connection. Also as a bonus the Jabber API now sets the person's nickname to the same as their character name. --- settings.py | 12 +- sso/services/jabber/__init__.py | 80 +-- sso/services/jabber/ejabberdctl.py | 239 ------- sso/services/jabber/xmpp/__init__.py | 31 - sso/services/jabber/xmpp/auth.py | 331 --------- sso/services/jabber/xmpp/browser.py | 221 ------ sso/services/jabber/xmpp/client.py | 325 --------- sso/services/jabber/xmpp/commands.py | 328 --------- sso/services/jabber/xmpp/debug.py | 423 ----------- sso/services/jabber/xmpp/dispatcher.py | 373 ---------- sso/services/jabber/xmpp/features.py | 182 ----- sso/services/jabber/xmpp/filetransfer.py | 199 ------ sso/services/jabber/xmpp/jep0106.py | 57 -- sso/services/jabber/xmpp/protocol.py | 860 ----------------------- sso/services/jabber/xmpp/roster.py | 184 ----- sso/services/jabber/xmpp/session.py | 349 --------- sso/services/jabber/xmpp/simplexml.py | 485 ------------- sso/services/jabber/xmpp/transports.py | 339 --------- sso/services/jabber/xmppclient.py | 184 ----- 19 files changed, 41 insertions(+), 5161 deletions(-) delete mode 100644 sso/services/jabber/ejabberdctl.py delete mode 100644 sso/services/jabber/xmpp/__init__.py delete mode 100644 sso/services/jabber/xmpp/auth.py delete mode 100644 sso/services/jabber/xmpp/browser.py delete mode 100644 sso/services/jabber/xmpp/client.py delete mode 100644 sso/services/jabber/xmpp/commands.py delete mode 100644 sso/services/jabber/xmpp/debug.py delete mode 100644 sso/services/jabber/xmpp/dispatcher.py delete mode 100644 sso/services/jabber/xmpp/features.py delete mode 100644 sso/services/jabber/xmpp/filetransfer.py delete mode 100644 sso/services/jabber/xmpp/jep0106.py delete mode 100644 sso/services/jabber/xmpp/protocol.py delete mode 100644 sso/services/jabber/xmpp/roster.py delete mode 100644 sso/services/jabber/xmpp/session.py delete mode 100644 sso/services/jabber/xmpp/simplexml.py delete mode 100644 sso/services/jabber/xmpp/transports.py delete mode 100644 sso/services/jabber/xmppclient.py diff --git a/settings.py b/settings.py index bde9378..c483318 100644 --- a/settings.py +++ b/settings.py @@ -110,16 +110,8 @@ REDDIT_PASSWD = '' # Vhost to add users to JABBER_SERVER = 'dredd.it' -# Method of communicating with the jabber server -# either 'xmpp' or 'cmd' -JABBER_METHOD = 'xmpp' - -# Use sudo? (cmd mode) -#JABBER_SUDO = True - -# Auth login user (xmpp mode) -JABBER_AUTH_USER = 'auth' -JABBER_AUTH_PASSWD = 'pepperllama34' +# XMLRPC url for ejabberd_xmlrpc +JABBER_XMLRPC_URL = 'http://127.0.0.1:4560' ### Mumble Service Settings diff --git a/sso/services/jabber/__init__.py b/sso/services/jabber/__init__.py index 6b3314f..6635b0b 100644 --- a/sso/services/jabber/__init__.py +++ b/sso/services/jabber/__init__.py @@ -1,10 +1,7 @@ +import xmlrpclib from sso.services import BaseService import settings -if settings.JABBER_METHOD == "xmpp": - from sso.services.jabber.xmppclient import JabberAdmin -else: - from sso.services.jabber.ejabberdctl import eJabberdCtl class JabberService(BaseService): @@ -12,62 +9,63 @@ class JabberService(BaseService): 'require_password': True, 'provide_login': False } - def __init__(self): - if settings.JABBER_METHOD == "xmpp": - self.method = "xmpp" - self.jabberadmin = JabberAdmin(settings.JABBER_SERVER, settings.JABBER_AUTH_USER, settings.JABBER_AUTH_PASSWD) - else: - self.method = "cmd" - self.ejctl = eJabberdCtl(sudo=settings.JABBER_SUDO) + @staticmethod + def exec_xmlrpc(func, **kwargs): + """ Send a XMLRPC request """ + server = xmlrpclib.Server(settings.JABBER_XMLRPC_URL) + params = {} + for i in kwargs: + params[i] = kwargs[i] + + return getattr(server, func)(params) def add_user(self, username, password, **kwargs): """ Add user to service """ - if self.method == "xmpp": - if self.jabberadmin.adduser('%s@%s' % (username, settings.JABBER_SERVER), password): - return '%s@%s' % (username, settings.JABBER_SERVER) + res = self.exec_xmlrpc('register', user=username, host=settings.JABBER_SERVER, password=password) + if res['res'] == 0: + if 'character' in kwargs: + self.exec_xmlrpc('set_nickname', user=username, host=settings.JABBER_SERVER, nickname=kwargs['character'].name) + return res['text'].split(" ")[1] else: - if self.ejctl.register(username.lower(), settings.JABBER_SERVER, password): - return '%s@%s' % (username, settings.JABBER_SERVER) - - def check_user(self, username): - """ Check if the username exists """ - if self.method == "xmpp": - return self.jabberadmin.checkuser("%s@%s" % (username, settings.JABBER_SERVER)) - elif username.lower() not in self.ejctl.get_users(settings.JABBER_SERVER): return False - else: - return True def delete_user(self, uid): """ Delete a user """ - if self.method == "xmpp": - return self.jabberadmin.deluser(uid) + username, server = uid.split("@") + res = self.exec_xmlrpc('unregister', user=username, host=server) + if res['res'] == 0: + return True else: - username, server = uid.split("@") - return self.ejctl.unregister(username, server) + return False + + def check_user(self, username): + """ Check if the username exists """ + res = self.exec_xmlrpc('check_account', user=username, host=settings.JABBER_SERVER) + if res['res'] == 0: + return True + else: + return False def disable_user(self, uid): """ Disable a user """ - if self.method == "xmpp": - return self.jabberadmin.disableuser(uid) + username, server = uid.split("@") + res = self.exec_xmlrpc('ban_account', host=server, user=username, reason='Auth account disable') + if res['res'] == 0: + return True else: - username, server = uid.split("@") - return self.ejctl.ban_user(server, username) + return False def enable_user(self, uid, password): """ Enable a user """ - if self.method == "xmpp": - return True - else: - username, server = uid.split("@") - return self.ejctl.enable_user(server, username, password) + self.reset_password(uid, password) def reset_password(self, uid, password): """ Reset the user's password """ - if self.method == "xmpp": - return self.jabberadmin.resetpassword(uid, password) + username, server = uid.split("@") + res = self.exec_xmlrpc('change_password', user=username, host=server, newpass=password) + if res['res'] == 0: + return True else: - username, server = uid.split("@") - return self.ejctl.set_password(server, username, password) + return False ServiceClass = 'JabberService' diff --git a/sso/services/jabber/ejabberdctl.py b/sso/services/jabber/ejabberdctl.py deleted file mode 100644 index e252f80..0000000 --- a/sso/services/jabber/ejabberdctl.py +++ /dev/null @@ -1,239 +0,0 @@ -import subprocess -import shlex -import StringIO - -class CommandError(): - def __init__(self, msg): - self.msg = msg - - def __str__(self): - return self.msg - -class eJabberdCtl(): - """ Python abstraction of ejabberdctl """ - - def __init__(self, sudo=False, ejctl='/usr/sbin/ejabberdctl'): - if sudo: - self.ejctl = ['sudo',ejctl] - else: - self.ejctl = [ejctl] - - def _execute(self, commandline): - """ Execute a ejabberd command """ - - args = [] - args.extend(self.ejctl) - args.extend(shlex.split(commandline.encode('ascii'))) - - # Convert all arguments to ascii first - #args = map(lambda x: x.encode('ascii'), args) - - print 'Executing: %s' % " ".join(args) - - try: - proc = subprocess.Popen(args, stdout=subprocess.PIPE) - proc.wait() - out = proc.communicate() - ret = proc.returncode - #print "%d: %s" % (ret, out) - except OSError, e: - raise CommandError('Error encountered during execution: %s' % e) - if ret > 0: - raise CommandError('Error: return code is %s' % ret) - elif ret < 0: - raise CommandError('Terminated by signal %s' % ret) - - return out[0] - - def status(): - """ Returns the server status """ - pass - - def stop(): - """ Stop the ejabberd server """ - pass - - def restart(): - """ Restart the ejabberd server """ - pass - - def reopen_log(): - """ Reopen the ejabberd log file """ - pass - - def register(self, user, server, password): - """ Adds a user to a vhost """ - - cmd = 'register %s %s %s' % (user, server, password) - - try: - self._execute(cmd) - except CommandError, e: - return False - else: - return True - - def unregister(self, user, server): - """ Deletes a user from a vhost """ - - cmd = "unregister %s %s" % (user, server) - - try: - self._execute(cmd) - except CommandError, e: - return False - else: - return True - - def backup(self, file): - """ Backup the ejabberd database to a file """ - pass - - def restore(self, file): - """ Restore a backup file to the database """ - pass - - def install_fallback(self, file): - """ Install a database fallback file """ - pass - - def dump(self, file): - """ Dump the database to a text file """ - pass - - def load(self, file): - """ Load a database from a text file """ - pass - - def delete_expired_messages(self): - """ Delete expired offline messages from the database """ - - cmd = "delete-expired-messages" - - try: - self._execute(cmd) - except CommandError, e: - return False - else: - return True - - def delete_old_messages(self, n): - """ Delete offline messages older than n days from the database """ - cmd = "delete-old-messages %s" % n - - try: - self._execute(cmd) - except CommandError, e: - return False - else: - return True - - def status_list(self, status): - """ Gets a list of users with the specified status """ - - cmd = "status-list %s" % status - - try: - cmdout = self._execute(cmd) - except CommandError, e: - return 0 - else: - out = {} - lines = cmdout[:-1].split('\n') - for item in lines: - u = item.split(' ') - if not hasattr(out, u[0]): - out[u[0]] = {} - out[u[0]][u[1]] = u[2:] - - return out - - def set_password(self, user, server, password): - - cmd = "set-password %s %s %s" % (user, server, password) - - try: - self._execute(cmd) - except CommandError, e: - return False - else: - return True - - def is_online(self, user, server): - """ Returns a boolean of if the user is currently online """ - - cmd = "user-resources %s %s" % (user, server) - - try: - out = self._execute(cmd) - except CommandError, e: - return False - else: - if len(out) > 0: - return True - else: - return False - - def online_users(self, server=None): - """ Gives the number of online users server-wide or for a particular vhost """ - - if server: - cmd = "vhost %s stats onlineusers" % server - else: - cmd = "connected-users-number" - - try: - out = self._execute(cmd) - except CommandError, e: - return 0 - else: - return int(out) - - def get_users(self, server): - """ Gets a list of users for a specific vhost """ - - cmd = "vhost %s registered-users" % server - try: - out = self._execute(cmd) - except CommandError, e: - return [] - else: - return out[:-1].split('\n') - - - def get_shared_groups(self, server): - """ Gets a list of Shared Roster Groups (SRGs) for a specific vhost """ - - cmd = "srg-list-groups %s" % server - - try: - out = self._execute(cmd) - except CommandError, e: - return 0 - else: - return out[:-1].split('\n') - - def ban_user(self, server, user, reason="Banned"): - """ Bans a user, and kicks them off the server """ - - cmd = "vhost %s ban-account %s %s" % (server, user, reason) - - try: - self._execute(cmd) - except CommandError, e: - return False - else: - return True - -if __name__ == '__main__': - b = eJabberdCtl(sudo=True) - - print b.register('test88','dredd.it','bleh') - print b.is_online('matalok', 'dredd.it') - - print b.online_users() - print b.online_users('dredd.it') - print b.get_shared_groups('dredd.it') - print b.get_users('dredd.it') - print b.status_list('available') - print b.set_password('matalok', 'dredd.it', 'br6feoot') diff --git a/sso/services/jabber/xmpp/__init__.py b/sso/services/jabber/xmpp/__init__.py deleted file mode 100644 index ad03b28..0000000 --- a/sso/services/jabber/xmpp/__init__.py +++ /dev/null @@ -1,31 +0,0 @@ -# $Id: __init__.py,v 1.9 2005/03/07 09:34:51 snakeru Exp $ - -""" -All features of xmpppy library contained within separate modules. -At present there are modules: -simplexml - XML handling routines -protocol - jabber-objects (I.e. JID and different stanzas and sub-stanzas) handling routines. -debug - Jacob Lundquist's debugging module. Very handy if you like colored debug. -auth - Non-SASL and SASL stuff. You will need it to auth as a client or transport. -transports - low level connection handling. TCP and TLS currently. HTTP support planned. -roster - simple roster for use in clients. -dispatcher - decision-making logic. Handles all hooks. The first who takes control over fresh stanzas. -features - different stuff that didn't worths separating into modules -browser - DISCO server framework. Allows to build dynamic disco tree. -filetransfer - Currently contains only IBB stuff. Can be used for bot-to-bot transfers. - -Most of the classes that is defined in all these modules is an ancestors of -class PlugIn so they share a single set of methods allowing you to compile -a featured XMPP client. For every instance of PlugIn class the 'owner' is the class -in what the plug was plugged. While plugging in such instance usually sets some -methods of owner to it's own ones for easy access. All session specific info stored -either in instance of PlugIn or in owner's instance. This is considered unhandy -and there are plans to port 'Session' class from xmppd.py project for storing all -session-related info. Though if you are not accessing instances variables directly -and use only methods for access all values you should not have any problems. - -""" - -import simplexml,protocol,debug,auth,transports,roster,dispatcher,features,browser,filetransfer,commands -from client import * -from protocol import * diff --git a/sso/services/jabber/xmpp/auth.py b/sso/services/jabber/xmpp/auth.py deleted file mode 100644 index 825b36c..0000000 --- a/sso/services/jabber/xmpp/auth.py +++ /dev/null @@ -1,331 +0,0 @@ -## auth.py -## -## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - -# $Id: auth.py,v 1.41 2008/09/13 21:45:21 normanr Exp $ - -""" -Provides library with all Non-SASL and SASL authentication mechanisms. -Can be used both for client and transport authentication. -""" - -from protocol import * -from client import PlugIn -import sha,base64,random,dispatcher,re - -try: - from hashlib import md5 -except ImportError: - import md5 as md5lib - def md5(val): return md5lib.new(val) - -def HH(some): return md5(some).hexdigest() -def H(some): return md5(some).digest() -def C(some): return ':'.join(some) - -class NonSASL(PlugIn): - """ Implements old Non-SASL (JEP-0078) authentication used in jabberd1.4 and transport authentication.""" - def __init__(self,user,password,resource): - """ Caches username, password and resource for auth. """ - PlugIn.__init__(self) - self.DBG_LINE='gen_auth' - self.user=user - self.password=password - self.resource=resource - - def plugin(self,owner): - """ Determine the best auth method (digest/0k/plain) and use it for auth. - Returns used method name on success. Used internally. """ - if not self.resource: return self.authComponent(owner) - self.DEBUG('Querying server about possible auth methods','start') - resp=owner.Dispatcher.SendAndWaitForResponse(Iq('get',NS_AUTH,payload=[Node('username',payload=[self.user])])) - if not isResultNode(resp): - self.DEBUG('No result node arrived! Aborting...','error') - return - iq=Iq(typ='set',node=resp) - query=iq.getTag('query') - query.setTagData('username',self.user) - query.setTagData('resource',self.resource) - - if query.getTag('digest'): - self.DEBUG("Performing digest authentication",'ok') - query.setTagData('digest',sha.new(owner.Dispatcher.Stream._document_attrs['id']+self.password).hexdigest()) - if query.getTag('password'): query.delChild('password') - method='digest' - elif query.getTag('token'): - token=query.getTagData('token') - seq=query.getTagData('sequence') - self.DEBUG("Performing zero-k authentication",'ok') - hash = sha.new(sha.new(self.password).hexdigest()+token).hexdigest() - for foo in xrange(int(seq)): hash = sha.new(hash).hexdigest() - query.setTagData('hash',hash) - method='0k' - else: - self.DEBUG("Sequre methods unsupported, performing plain text authentication",'warn') - query.setTagData('password',self.password) - method='plain' - resp=owner.Dispatcher.SendAndWaitForResponse(iq) - if isResultNode(resp): - self.DEBUG('Sucessfully authenticated with remove host.','ok') - owner.User=self.user - owner.Resource=self.resource - owner._registered_name=owner.User+'@'+owner.Server+'/'+owner.Resource - return method - self.DEBUG('Authentication failed!','error') - - def authComponent(self,owner): - """ Authenticate component. Send handshake stanza and wait for result. Returns "ok" on success. """ - self.handshake=0 - owner.send(Node(NS_COMPONENT_ACCEPT+' handshake',payload=[sha.new(owner.Dispatcher.Stream._document_attrs['id']+self.password).hexdigest()])) - owner.RegisterHandler('handshake',self.handshakeHandler,xmlns=NS_COMPONENT_ACCEPT) - while not self.handshake: - self.DEBUG("waiting on handshake",'notify') - owner.Process(1) - owner._registered_name=self.user - if self.handshake+1: return 'ok' - - def handshakeHandler(self,disp,stanza): - """ Handler for registering in dispatcher for accepting transport authentication. """ - if stanza.getName()=='handshake': self.handshake=1 - else: self.handshake=-1 - -class SASL(PlugIn): - """ Implements SASL authentication. """ - def __init__(self,username,password): - PlugIn.__init__(self) - self.username=username - self.password=password - - def plugin(self,owner): - if not self._owner.Dispatcher.Stream._document_attrs.has_key('version'): self.startsasl='not-supported' - elif self._owner.Dispatcher.Stream.features: - try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features) - except NodeProcessed: pass - else: self.startsasl=None - - def auth(self): - """ Start authentication. Result can be obtained via "SASL.startsasl" attribute and will be - either "success" or "failure". Note that successfull auth will take at least - two Dispatcher.Process() calls. """ - if self.startsasl: pass - elif self._owner.Dispatcher.Stream.features: - try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features) - except NodeProcessed: pass - else: self._owner.RegisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) - - def plugout(self): - """ Remove SASL handlers from owner's dispatcher. Used internally. """ - if self._owner.__dict__.has_key('features'): self._owner.UnregisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) - if self._owner.__dict__.has_key('challenge'): self._owner.UnregisterHandler('challenge',self.SASLHandler,xmlns=NS_SASL) - if self._owner.__dict__.has_key('failure'): self._owner.UnregisterHandler('failure',self.SASLHandler,xmlns=NS_SASL) - if self._owner.__dict__.has_key('success'): self._owner.UnregisterHandler('success',self.SASLHandler,xmlns=NS_SASL) - - def FeaturesHandler(self,conn,feats): - """ Used to determine if server supports SASL auth. Used internally. """ - if not feats.getTag('mechanisms',namespace=NS_SASL): - self.startsasl='not-supported' - self.DEBUG('SASL not supported by server','error') - return - mecs=[] - for mec in feats.getTag('mechanisms',namespace=NS_SASL).getTags('mechanism'): - mecs.append(mec.getData()) - self._owner.RegisterHandler('challenge',self.SASLHandler,xmlns=NS_SASL) - self._owner.RegisterHandler('failure',self.SASLHandler,xmlns=NS_SASL) - self._owner.RegisterHandler('success',self.SASLHandler,xmlns=NS_SASL) - if "ANONYMOUS" in mecs and self.username == None: - node=Node('auth',attrs={'xmlns':NS_SASL,'mechanism':'ANONYMOUS'}) - elif "DIGEST-MD5" in mecs: - node=Node('auth',attrs={'xmlns':NS_SASL,'mechanism':'DIGEST-MD5'}) - elif "PLAIN" in mecs: - sasl_data='%s\x00%s\x00%s'%(self.username+'@'+self._owner.Server,self.username,self.password) - node=Node('auth',attrs={'xmlns':NS_SASL,'mechanism':'PLAIN'},payload=[base64.encodestring(sasl_data).replace('\r','').replace('\n','')]) - else: - self.startsasl='failure' - self.DEBUG('I can only use DIGEST-MD5 and PLAIN mecanisms.','error') - return - self.startsasl='in-process' - self._owner.send(node.__str__()) - raise NodeProcessed - - def SASLHandler(self,conn,challenge): - """ Perform next SASL auth step. Used internally. """ - if challenge.getNamespace()<>NS_SASL: return - if challenge.getName()=='failure': - self.startsasl='failure' - try: reason=challenge.getChildren()[0] - except: reason=challenge - self.DEBUG('Failed SASL authentification: %s'%reason,'error') - raise NodeProcessed - elif challenge.getName()=='success': - self.startsasl='success' - self.DEBUG('Successfully authenticated with remote server.','ok') - handlers=self._owner.Dispatcher.dumpHandlers() - self._owner.Dispatcher.PlugOut() - dispatcher.Dispatcher().PlugIn(self._owner) - self._owner.Dispatcher.restoreHandlers(handlers) - self._owner.User=self.username - raise NodeProcessed -########################################3333 - incoming_data=challenge.getData() - chal={} - data=base64.decodestring(incoming_data) - self.DEBUG('Got challenge:'+data,'ok') - for pair in re.findall('(\w+\s*=\s*(?:(?:"[^"]+")|(?:[^,]+)))',data): - key,value=[x.strip() for x in pair.split('=', 1)] - if value[:1]=='"' and value[-1:]=='"': value=value[1:-1] - chal[key]=value - if chal.has_key('qop') and 'auth' in [x.strip() for x in chal['qop'].split(',')]: - resp={} - resp['username']=self.username - resp['realm']=self._owner.Server - resp['nonce']=chal['nonce'] - cnonce='' - for i in range(7): - cnonce+=hex(int(random.random()*65536*4096))[2:] - resp['cnonce']=cnonce - resp['nc']=('00000001') - resp['qop']='auth' - resp['digest-uri']='xmpp/'+self._owner.Server - A1=C([H(C([resp['username'],resp['realm'],self.password])),resp['nonce'],resp['cnonce']]) - A2=C(['AUTHENTICATE',resp['digest-uri']]) - response= HH(C([HH(A1),resp['nonce'],resp['nc'],resp['cnonce'],resp['qop'],HH(A2)])) - resp['response']=response - resp['charset']='utf-8' - sasl_data='' - for key in ['charset','username','realm','nonce','nc','cnonce','digest-uri','response','qop']: - if key in ['nc','qop','response','charset']: sasl_data+="%s=%s,"%(key,resp[key]) - else: sasl_data+='%s="%s",'%(key,resp[key]) -########################################3333 - node=Node('response',attrs={'xmlns':NS_SASL},payload=[base64.encodestring(sasl_data[:-1]).replace('\r','').replace('\n','')]) - self._owner.send(node.__str__()) - elif chal.has_key('rspauth'): self._owner.send(Node('response',attrs={'xmlns':NS_SASL}).__str__()) - else: - self.startsasl='failure' - self.DEBUG('Failed SASL authentification: unknown challenge','error') - raise NodeProcessed - -class Bind(PlugIn): - """ Bind some JID to the current connection to allow router know of our location.""" - def __init__(self): - PlugIn.__init__(self) - self.DBG_LINE='bind' - self.bound=None - - def plugin(self,owner): - """ Start resource binding, if allowed at this time. Used internally. """ - if self._owner.Dispatcher.Stream.features: - try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features) - except NodeProcessed: pass - else: self._owner.RegisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) - - def plugout(self): - """ Remove Bind handler from owner's dispatcher. Used internally. """ - self._owner.UnregisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) - - def FeaturesHandler(self,conn,feats): - """ Determine if server supports resource binding and set some internal attributes accordingly. """ - if not feats.getTag('bind',namespace=NS_BIND): - self.bound='failure' - self.DEBUG('Server does not requested binding.','error') - return - if feats.getTag('session',namespace=NS_SESSION): self.session=1 - else: self.session=-1 - self.bound=[] - - def Bind(self,resource=None): - """ Perform binding. Use provided resource name or random (if not provided). """ - while self.bound is None and self._owner.Process(1): pass - if resource: resource=[Node('resource',payload=[resource])] - else: resource=[] - resp=self._owner.SendAndWaitForResponse(Protocol('iq',typ='set',payload=[Node('bind',attrs={'xmlns':NS_BIND},payload=resource)])) - if isResultNode(resp): - self.bound.append(resp.getTag('bind').getTagData('jid')) - self.DEBUG('Successfully bound %s.'%self.bound[-1],'ok') - jid=JID(resp.getTag('bind').getTagData('jid')) - self._owner.User=jid.getNode() - self._owner.Resource=jid.getResource() - resp=self._owner.SendAndWaitForResponse(Protocol('iq',typ='set',payload=[Node('session',attrs={'xmlns':NS_SESSION})])) - if isResultNode(resp): - self.DEBUG('Successfully opened session.','ok') - self.session=1 - return 'ok' - else: - self.DEBUG('Session open failed.','error') - self.session=0 - elif resp: self.DEBUG('Binding failed: %s.'%resp.getTag('error'),'error') - else: - self.DEBUG('Binding failed: timeout expired.','error') - return '' - -class ComponentBind(PlugIn): - """ ComponentBind some JID to the current connection to allow router know of our location.""" - def __init__(self, sasl): - PlugIn.__init__(self) - self.DBG_LINE='bind' - self.bound=None - self.needsUnregister=None - self.sasl = sasl - - def plugin(self,owner): - """ Start resource binding, if allowed at this time. Used internally. """ - if not self.sasl: - self.bound=[] - return - if self._owner.Dispatcher.Stream.features: - try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features) - except NodeProcessed: pass - else: - self._owner.RegisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) - self.needsUnregister=1 - - def plugout(self): - """ Remove ComponentBind handler from owner's dispatcher. Used internally. """ - if self.needsUnregister: - self._owner.UnregisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) - - def FeaturesHandler(self,conn,feats): - """ Determine if server supports resource binding and set some internal attributes accordingly. """ - if not feats.getTag('bind',namespace=NS_BIND): - self.bound='failure' - self.DEBUG('Server does not requested binding.','error') - return - if feats.getTag('session',namespace=NS_SESSION): self.session=1 - else: self.session=-1 - self.bound=[] - - def Bind(self,domain=None): - """ Perform binding. Use provided domain name (if not provided). """ - while self.bound is None and self._owner.Process(1): pass - if self.sasl: - xmlns = NS_COMPONENT_1 - else: - xmlns = None - self.bindresponse = None - ttl = dispatcher.DefaultTimeout - self._owner.RegisterHandler('bind',self.BindHandler,xmlns=xmlns) - self._owner.send(Protocol('bind',attrs={'name':domain},xmlns=NS_COMPONENT_1)) - while self.bindresponse is None and self._owner.Process(1) and ttl > 0: ttl-=1 - self._owner.UnregisterHandler('bind',self.BindHandler,xmlns=xmlns) - resp=self.bindresponse - if resp and resp.getAttr('error'): - self.DEBUG('Binding failed: %s.'%resp.getAttr('error'),'error') - elif resp: - self.DEBUG('Successfully bound.','ok') - return 'ok' - else: - self.DEBUG('Binding failed: timeout expired.','error') - return '' - - def BindHandler(self,conn,bind): - self.bindresponse = bind - pass diff --git a/sso/services/jabber/xmpp/browser.py b/sso/services/jabber/xmpp/browser.py deleted file mode 100644 index 8848ea4..0000000 --- a/sso/services/jabber/xmpp/browser.py +++ /dev/null @@ -1,221 +0,0 @@ -## browser.py -## -## Copyright (C) 2004 Alexey "Snake" Nezhdanov -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - -# $Id: browser.py,v 1.12 2007/05/13 17:55:14 normanr Exp $ - -"""Browser module provides DISCO server framework for your application. -This functionality can be used for very different purposes - from publishing -software version and supported features to building of "jabber site" that users -can navigate with their disco browsers and interact with active content. - -Such functionality is achieved via registering "DISCO handlers" that are -automatically called when user requests some node of your disco tree. -""" - -from dispatcher import * -from client import PlugIn - -class Browser(PlugIn): - """ WARNING! This class is for components only. It will not work in client mode! - - Standart xmpppy class that is ancestor of PlugIn and can be attached - to your application. - All processing will be performed in the handlers registered in the browser - instance. You can register any number of handlers ensuring that for each - node/jid combination only one (or none) handler registered. - You can register static information or the fully-blown function that will - calculate the answer dynamically. - Example of static info (see JEP-0030, examples 13-14): - # cl - your xmpppy connection instance. - b=xmpp.browser.Browser() - b.PlugIn(cl) - items=[] - item={} - item['jid']='catalog.shakespeare.lit' - item['node']='books' - item['name']='Books by and about Shakespeare' - items.append(item) - item={} - item['jid']='catalog.shakespeare.lit' - item['node']='clothing' - item['name']='Wear your literary taste with pride' - items.append(item) - item={} - item['jid']='catalog.shakespeare.lit' - item['node']='music' - item['name']='Music from the time of Shakespeare' - items.append(item) - info={'ids':[], 'features':[]} - b.setDiscoHandler({'items':items,'info':info}) - - items should be a list of item elements. - every item element can have any of these four keys: 'jid', 'node', 'name', 'action' - info should be a dicionary and must have keys 'ids' and 'features'. - Both of them should be lists: - ids is a list of dictionaries and features is a list of text strings. - Example (see JEP-0030, examples 1-2) - # cl - your xmpppy connection instance. - b=xmpp.browser.Browser() - b.PlugIn(cl) - items=[] - ids=[] - ids.append({'category':'conference','type':'text','name':'Play-Specific Chatrooms'}) - ids.append({'category':'directory','type':'chatroom','name':'Play-Specific Chatrooms'}) - features=[NS_DISCO_INFO,NS_DISCO_ITEMS,NS_MUC,NS_REGISTER,NS_SEARCH,NS_TIME,NS_VERSION] - info={'ids':ids,'features':features} - # info['xdata']=xmpp.protocol.DataForm() # JEP-0128 - b.setDiscoHandler({'items':[],'info':info}) - """ - def __init__(self): - """Initialises internal variables. Used internally.""" - PlugIn.__init__(self) - DBG_LINE='browser' - self._exported_methods=[] - self._handlers={'':{}} - - def plugin(self, owner): - """ Registers it's own iq handlers in your application dispatcher instance. - Used internally.""" - owner.RegisterHandler('iq',self._DiscoveryHandler,typ='get',ns=NS_DISCO_INFO) - owner.RegisterHandler('iq',self._DiscoveryHandler,typ='get',ns=NS_DISCO_ITEMS) - - def plugout(self): - """ Unregisters browser's iq handlers from your application dispatcher instance. - Used internally.""" - self._owner.UnregisterHandler('iq',self._DiscoveryHandler,typ='get',ns=NS_DISCO_INFO) - self._owner.UnregisterHandler('iq',self._DiscoveryHandler,typ='get',ns=NS_DISCO_ITEMS) - - def _traversePath(self,node,jid,set=0): - """ Returns dictionary and key or None,None - None - root node (w/o "node" attribute) - /a/b/c - node - /a/b/ - branch - Set returns '' or None as the key - get returns '' or None as the key or None as the dict. - Used internally.""" - if self._handlers.has_key(jid): cur=self._handlers[jid] - elif set: - self._handlers[jid]={} - cur=self._handlers[jid] - else: cur=self._handlers[''] - if node is None: node=[None] - else: node=node.replace('/',' /').split('/') - for i in node: - if i<>'' and cur.has_key(i): cur=cur[i] - elif set and i<>'': cur[i]={dict:cur,str:i}; cur=cur[i] - elif set or cur.has_key(''): return cur,'' - else: return None,None - if cur.has_key(1) or set: return cur,1 - raise "Corrupted data" - - def setDiscoHandler(self,handler,node='',jid=''): - """ This is the main method that you will use in this class. - It is used to register supplied DISCO handler (or dictionary with static info) - as handler of some disco tree branch. - If you do not specify the node this handler will be used for all queried nodes. - If you do not specify the jid this handler will be used for all queried JIDs. - - Usage: - cl.Browser.setDiscoHandler(someDict,node,jid) - or - cl.Browser.setDiscoHandler(someDISCOHandler,node,jid) - where - - someDict={ - 'items':[ - {'jid':'jid1','action':'action1','node':'node1','name':'name1'}, - {'jid':'jid2','action':'action2','node':'node2','name':'name2'}, - {'jid':'jid3','node':'node3','name':'name3'}, - {'jid':'jid4','node':'node4'} - ], - 'info' :{ - 'ids':[ - {'category':'category1','type':'type1','name':'name1'}, - {'category':'category2','type':'type2','name':'name2'}, - {'category':'category3','type':'type3','name':'name3'}, - ], - 'features':['feature1','feature2','feature3','feature4'], - 'xdata':DataForm - } - } - - and/or - - def someDISCOHandler(session,request,TYR): - # if TYR=='items': # returns items list of the same format as shown above - # elif TYR=='info': # returns info dictionary of the same format as shown above - # else: # this case is impossible for now. - """ - self.DEBUG('Registering handler %s for "%s" node->%s'%(handler,jid,node), 'info') - node,key=self._traversePath(node,jid,1) - node[key]=handler - - def getDiscoHandler(self,node='',jid=''): - """ Returns the previously registered DISCO handler - that is resonsible for this node/jid combination. - Used internally.""" - node,key=self._traversePath(node,jid) - if node: return node[key] - - def delDiscoHandler(self,node='',jid=''): - """ Unregisters DISCO handler that is resonsible for this - node/jid combination. When handler is unregistered the branch - is handled in the same way that it's parent branch from this moment. - """ - node,key=self._traversePath(node,jid) - if node: - handler=node[key] - del node[dict][node[str]] - return handler - - def _DiscoveryHandler(self,conn,request): - """ Servers DISCO iq request from the remote client. - Automatically determines the best handler to use and calls it - to handle the request. Used internally. - """ - node=request.getQuerynode() - if node: - nodestr=node - else: - nodestr='None' - handler=self.getDiscoHandler(node,request.getTo()) - if not handler: - self.DEBUG("No Handler for request with jid->%s node->%s ns->%s"%(request.getTo().__str__().encode('utf8'),nodestr.encode('utf8'),request.getQueryNS().encode('utf8')),'error') - conn.send(Error(request,ERR_ITEM_NOT_FOUND)) - raise NodeProcessed - self.DEBUG("Handling request with jid->%s node->%s ns->%s"%(request.getTo().__str__().encode('utf8'),nodestr.encode('utf8'),request.getQueryNS().encode('utf8')),'ok') - rep=request.buildReply('result') - if node: rep.setQuerynode(node) - q=rep.getTag('query') - if request.getQueryNS()==NS_DISCO_ITEMS: - # handler must return list: [{jid,action,node,name}] - if type(handler)==dict: lst=handler['items'] - else: lst=handler(conn,request,'items') - if lst==None: - conn.send(Error(request,ERR_ITEM_NOT_FOUND)) - raise NodeProcessed - for item in lst: q.addChild('item',item) - elif request.getQueryNS()==NS_DISCO_INFO: - if type(handler)==dict: dt=handler['info'] - else: dt=handler(conn,request,'info') - if dt==None: - conn.send(Error(request,ERR_ITEM_NOT_FOUND)) - raise NodeProcessed - # handler must return dictionary: - # {'ids':[{},{},{},{}], 'features':[fe,at,ur,es], 'xdata':DataForm} - for id in dt['ids']: q.addChild('identity',id) - for feature in dt['features']: q.addChild('feature',{'var':feature}) - if dt.has_key('xdata'): q.addChild(node=dt['xdata']) - conn.send(rep) - raise NodeProcessed diff --git a/sso/services/jabber/xmpp/client.py b/sso/services/jabber/xmpp/client.py deleted file mode 100644 index 4d93211..0000000 --- a/sso/services/jabber/xmpp/client.py +++ /dev/null @@ -1,325 +0,0 @@ -## client.py -## -## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - -# $Id: client.py,v 1.61 2009/04/07 06:19:42 snakeru Exp $ - -""" -Provides PlugIn class functionality to develop extentions for xmpppy. -Also provides Client and Component classes implementations as the -examples of xmpppy structures usage. -These classes can be used for simple applications "AS IS" though. -""" - -import socket -import debug -Debug=debug -Debug.DEBUGGING_IS_ON=1 -Debug.Debug.colors['socket']=debug.color_dark_gray -Debug.Debug.colors['CONNECTproxy']=debug.color_dark_gray -Debug.Debug.colors['nodebuilder']=debug.color_brown -Debug.Debug.colors['client']=debug.color_cyan -Debug.Debug.colors['component']=debug.color_cyan -Debug.Debug.colors['dispatcher']=debug.color_green -Debug.Debug.colors['browser']=debug.color_blue -Debug.Debug.colors['auth']=debug.color_yellow -Debug.Debug.colors['roster']=debug.color_magenta -Debug.Debug.colors['ibb']=debug.color_yellow - -Debug.Debug.colors['down']=debug.color_brown -Debug.Debug.colors['up']=debug.color_brown -Debug.Debug.colors['data']=debug.color_brown -Debug.Debug.colors['ok']=debug.color_green -Debug.Debug.colors['warn']=debug.color_yellow -Debug.Debug.colors['error']=debug.color_red -Debug.Debug.colors['start']=debug.color_dark_gray -Debug.Debug.colors['stop']=debug.color_dark_gray -Debug.Debug.colors['sent']=debug.color_yellow -Debug.Debug.colors['got']=debug.color_bright_cyan - -DBG_CLIENT='client' -DBG_COMPONENT='component' - -class PlugIn: - """ Common xmpppy plugins infrastructure: plugging in/out, debugging. """ - def __init__(self): - self._exported_methods=[] - self.DBG_LINE=self.__class__.__name__.lower() - - def PlugIn(self,owner): - """ Attach to main instance and register ourself and all our staff in it. """ - self._owner=owner - if self.DBG_LINE not in owner.debug_flags: - owner.debug_flags.append(self.DBG_LINE) - self.DEBUG('Plugging %s into %s'%(self,self._owner),'start') - if owner.__dict__.has_key(self.__class__.__name__): - return self.DEBUG('Plugging ignored: another instance already plugged.','error') - self._old_owners_methods=[] - for method in self._exported_methods: - if owner.__dict__.has_key(method.__name__): - self._old_owners_methods.append(owner.__dict__[method.__name__]) - owner.__dict__[method.__name__]=method - owner.__dict__[self.__class__.__name__]=self - if self.__class__.__dict__.has_key('plugin'): return self.plugin(owner) - - def PlugOut(self): - """ Unregister all our staff from main instance and detach from it. """ - self.DEBUG('Plugging %s out of %s.'%(self,self._owner),'stop') - ret = None - if self.__class__.__dict__.has_key('plugout'): ret = self.plugout() - self._owner.debug_flags.remove(self.DBG_LINE) - for method in self._exported_methods: del self._owner.__dict__[method.__name__] - for method in self._old_owners_methods: self._owner.__dict__[method.__name__]=method - del self._owner.__dict__[self.__class__.__name__] - return ret - - def DEBUG(self,text,severity='info'): - """ Feed a provided debug line to main instance's debug facility along with our ID string. """ - self._owner.DEBUG(self.DBG_LINE,text,severity) - -import transports,dispatcher,auth,roster -class CommonClient: - """ Base for Client and Component classes.""" - def __init__(self,server,port=5222,debug=['always', 'nodebuilder']): - """ Caches server name and (optionally) port to connect to. "debug" parameter specifies - the debug IDs that will go into debug output. You can either specifiy an "include" - or "exclude" list. The latter is done via adding "always" pseudo-ID to the list. - Full list: ['nodebuilder', 'dispatcher', 'gen_auth', 'SASL_auth', 'bind', 'socket', - 'CONNECTproxy', 'TLS', 'roster', 'browser', 'ibb'] . """ - if self.__class__.__name__=='Client': self.Namespace,self.DBG='jabber:client',DBG_CLIENT - elif self.__class__.__name__=='Component': self.Namespace,self.DBG=dispatcher.NS_COMPONENT_ACCEPT,DBG_COMPONENT - self.defaultNamespace=self.Namespace - self.disconnect_handlers=[] - self.Server=server - self.Port=port - if debug and type(debug)<>list: debug=['always', 'nodebuilder'] - self._DEBUG=Debug.Debug(debug) - self.DEBUG=self._DEBUG.Show - self.debug_flags=self._DEBUG.debug_flags - self.debug_flags.append(self.DBG) - self._owner=self - self._registered_name=None - self.RegisterDisconnectHandler(self.DisconnectHandler) - self.connected='' - self._route=0 - - def RegisterDisconnectHandler(self,handler): - """ Register handler that will be called on disconnect.""" - self.disconnect_handlers.append(handler) - - def UnregisterDisconnectHandler(self,handler): - """ Unregister handler that is called on disconnect.""" - self.disconnect_handlers.remove(handler) - - def disconnected(self): - """ Called on disconnection. Calls disconnect handlers and cleans things up. """ - self.connected='' - self.DEBUG(self.DBG,'Disconnect detected','stop') - self.disconnect_handlers.reverse() - for i in self.disconnect_handlers: i() - self.disconnect_handlers.reverse() - if self.__dict__.has_key('TLS'): self.TLS.PlugOut() - - def DisconnectHandler(self): - """ Default disconnect handler. Just raises an IOError. - If you choosed to use this class in your production client, - override this method or at least unregister it. """ - raise IOError('Disconnected from server.') - - def event(self,eventName,args={}): - """ Default event handler. To be overriden. """ - print "Event: ",(eventName,args) - - def isConnected(self): - """ Returns connection state. F.e.: None / 'tls' / 'tcp+non_sasl' . """ - return self.connected - - def reconnectAndReauth(self): - """ Example of reconnection method. In fact, it can be used to batch connection and auth as well. """ - handlerssave=self.Dispatcher.dumpHandlers() - if self.__dict__.has_key('ComponentBind'): self.ComponentBind.PlugOut() - if self.__dict__.has_key('Bind'): self.Bind.PlugOut() - self._route=0 - if self.__dict__.has_key('NonSASL'): self.NonSASL.PlugOut() - if self.__dict__.has_key('SASL'): self.SASL.PlugOut() - if self.__dict__.has_key('TLS'): self.TLS.PlugOut() - self.Dispatcher.PlugOut() - if self.__dict__.has_key('HTTPPROXYsocket'): self.HTTPPROXYsocket.PlugOut() - if self.__dict__.has_key('TCPsocket'): self.TCPsocket.PlugOut() - if not self.connect(server=self._Server,proxy=self._Proxy): return - if not self.auth(self._User,self._Password,self._Resource): return - self.Dispatcher.restoreHandlers(handlerssave) - return self.connected - - def connect(self,server=None,proxy=None,ssl=None,use_srv=None): - """ Make a tcp/ip connection, protect it with tls/ssl if possible and start XMPP stream. - Returns None or 'tcp' or 'tls', depending on the result.""" - if not server: server=(self.Server,self.Port) - if proxy: sock=transports.HTTPPROXYsocket(proxy,server,use_srv) - else: sock=transports.TCPsocket(server,use_srv) - connected=sock.PlugIn(self) - if not connected: - sock.PlugOut() - return - self._Server,self._Proxy=server,proxy - self.connected='tcp' - if (ssl is None and self.Connection.getPort() in (5223, 443)) or ssl: - try: # FIXME. This should be done in transports.py - transports.TLS().PlugIn(self,now=1) - self.connected='ssl' - except socket.sslerror: - return - dispatcher.Dispatcher().PlugIn(self) - while self.Dispatcher.Stream._document_attrs is None: - if not self.Process(1): return - if self.Dispatcher.Stream._document_attrs.has_key('version') and self.Dispatcher.Stream._document_attrs['version']=='1.0': - while not self.Dispatcher.Stream.features and self.Process(1): pass # If we get version 1.0 stream the features tag MUST BE presented - return self.connected - -class Client(CommonClient): - """ Example client class, based on CommonClient. """ - def connect(self,server=None,proxy=None,secure=None,use_srv=True): - """ Connect to jabber server. If you want to specify different ip/port to connect to you can - pass it as tuple as first parameter. If there is HTTP proxy between you and server - specify it's address and credentials (if needed) in the second argument. - If you want ssl/tls support to be discovered and enable automatically - leave third argument as None. (ssl will be autodetected only if port is 5223 or 443) - If you want to force SSL start (i.e. if port 5223 or 443 is remapped to some non-standard port) then set it to 1. - If you want to disable tls/ssl support completely, set it to 0. - Example: connect(('192.168.5.5',5222),{'host':'proxy.my.net','port':8080,'user':'me','password':'secret'}) - Returns '' or 'tcp' or 'tls', depending on the result.""" - if not CommonClient.connect(self,server,proxy,secure,use_srv) or secure<>None and not secure: return self.connected - transports.TLS().PlugIn(self) - if not self.Dispatcher.Stream._document_attrs.has_key('version') or not self.Dispatcher.Stream._document_attrs['version']=='1.0': return self.connected - while not self.Dispatcher.Stream.features and self.Process(1): pass # If we get version 1.0 stream the features tag MUST BE presented - if not self.Dispatcher.Stream.features.getTag('starttls'): return self.connected # TLS not supported by server - while not self.TLS.starttls and self.Process(1): pass - if not hasattr(self, 'TLS') or self.TLS.starttls!='success': self.event('tls_failed'); return self.connected - self.connected='tls' - return self.connected - - def auth(self,user,password,resource='',sasl=1): - """ Authenticate connnection and bind resource. If resource is not provided - random one or library name used. """ - self._User,self._Password,self._Resource=user,password,resource - while not self.Dispatcher.Stream._document_attrs and self.Process(1): pass - if self.Dispatcher.Stream._document_attrs.has_key('version') and self.Dispatcher.Stream._document_attrs['version']=='1.0': - while not self.Dispatcher.Stream.features and self.Process(1): pass # If we get version 1.0 stream the features tag MUST BE presented - if sasl: auth.SASL(user,password).PlugIn(self) - if not sasl or self.SASL.startsasl=='not-supported': - if not resource: resource='xmpppy' - if auth.NonSASL(user,password,resource).PlugIn(self): - self.connected+='+old_auth' - return 'old_auth' - return - self.SASL.auth() - while self.SASL.startsasl=='in-process' and self.Process(1): pass - if self.SASL.startsasl=='success': - auth.Bind().PlugIn(self) - while self.Bind.bound is None and self.Process(1): pass - if self.Bind.Bind(resource): - self.connected+='+sasl' - return 'sasl' - else: - if self.__dict__.has_key('SASL'): self.SASL.PlugOut() - - def getRoster(self): - """ Return the Roster instance, previously plugging it in and - requesting roster from server if needed. """ - if not self.__dict__.has_key('Roster'): roster.Roster().PlugIn(self) - return self.Roster.getRoster() - - def sendInitPresence(self,requestRoster=1): - """ Send roster request and initial . - You can disable the first by setting requestRoster argument to 0. """ - self.sendPresence(requestRoster=requestRoster) - - def sendPresence(self,jid=None,typ=None,requestRoster=0): - """ Send some specific presence state. - Can also request roster from server if according agrument is set.""" - if requestRoster: roster.Roster().PlugIn(self) - self.send(dispatcher.Presence(to=jid, typ=typ)) - -class Component(CommonClient): - """ Component class. The only difference from CommonClient is ability to perform component authentication. """ - def __init__(self,transport,port=5347,typ=None,debug=['always', 'nodebuilder'],domains=None,sasl=0,bind=0,route=0,xcp=0): - """ Init function for Components. - As components use a different auth mechanism which includes the namespace of the component. - Jabberd1.4 and Ejabberd use the default namespace then for all client messages. - Jabberd2 uses jabber:client. - 'transport' argument is a transport name that you are going to serve (f.e. "irc.localhost"). - 'port' can be specified if 'transport' resolves to correct IP. If it is not then you'll have to specify IP - and port while calling "connect()". - If you are going to serve several different domains with single Component instance - you must list them ALL - in the 'domains' argument. - For jabberd2 servers you should set typ='jabberd2' argument. - """ - CommonClient.__init__(self,transport,port=port,debug=debug) - self.typ=typ - self.sasl=sasl - self.bind=bind - self.route=route - self.xcp=xcp - if domains: - self.domains=domains - else: - self.domains=[transport] - - def connect(self,server=None,proxy=None): - """ This will connect to the server, and if the features tag is found then set - the namespace to be jabber:client as that is required for jabberd2. - 'server' and 'proxy' arguments have the same meaning as in xmpp.Client.connect() """ - if self.sasl: - self.Namespace=auth.NS_COMPONENT_1 - self.Server=server[0] - CommonClient.connect(self,server=server,proxy=proxy) - if self.connected and (self.typ=='jabberd2' or not self.typ and self.Dispatcher.Stream.features != None) and (not self.xcp): - self.defaultNamespace=auth.NS_CLIENT - self.Dispatcher.RegisterNamespace(self.defaultNamespace) - self.Dispatcher.RegisterProtocol('iq',dispatcher.Iq) - self.Dispatcher.RegisterProtocol('message',dispatcher.Message) - self.Dispatcher.RegisterProtocol('presence',dispatcher.Presence) - return self.connected - - def dobind(self, sasl): - # This has to be done before binding, because we can receive a route stanza before binding finishes - self._route = self.route - if self.bind: - for domain in self.domains: - auth.ComponentBind(sasl).PlugIn(self) - while self.ComponentBind.bound is None: self.Process(1) - if (not self.ComponentBind.Bind(domain)): - self.ComponentBind.PlugOut() - return - self.ComponentBind.PlugOut() - - def auth(self,name,password,dup=None): - """ Authenticate component "name" with password "password".""" - self._User,self._Password,self._Resource=name,password,'' - try: - if self.sasl: auth.SASL(name,password).PlugIn(self) - if not self.sasl or self.SASL.startsasl=='not-supported': - if auth.NonSASL(name,password,'').PlugIn(self): - self.dobind(sasl=False) - self.connected+='+old_auth' - return 'old_auth' - return - self.SASL.auth() - while self.SASL.startsasl=='in-process' and self.Process(1): pass - if self.SASL.startsasl=='success': - self.dobind(sasl=True) - self.connected+='+sasl' - return 'sasl' - else: - raise auth.NotAuthorized(self.SASL.startsasl) - except: - self.DEBUG(self.DBG,"Failed to authenticate %s"%name,'error') diff --git a/sso/services/jabber/xmpp/commands.py b/sso/services/jabber/xmpp/commands.py deleted file mode 100644 index cdebf8f..0000000 --- a/sso/services/jabber/xmpp/commands.py +++ /dev/null @@ -1,328 +0,0 @@ -## $Id: commands.py,v 1.17 2007/08/28 09:54:15 normanr Exp $ - -## Ad-Hoc Command manager -## Mike Albon (c) 5th January 2005 - -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - - -"""This module is a ad-hoc command processor for xmpppy. It uses the plug-in mechanism like most of the core library. It depends on a DISCO browser manager. - -There are 3 classes here, a command processor Commands like the Browser, and a command template plugin Command, and an example command. - -To use this module: - - Instansiate the module with the parent transport and disco browser manager as parameters. - 'Plug in' commands using the command template. - The command feature must be added to existing disco replies where neccessary. - -What it supplies: - - Automatic command registration with the disco browser manager. - Automatic listing of commands in the public command list. - A means of handling requests, by redirection though the command manager. -""" - -from protocol import * -from client import PlugIn - -class Commands(PlugIn): - """Commands is an ancestor of PlugIn and can be attached to any session. - - The commands class provides a lookup and browse mechnism. It follows the same priciple of the Browser class, for Service Discovery to provide the list of commands, it adds the 'list' disco type to your existing disco handler function. - - How it works: - The commands are added into the existing Browser on the correct nodes. When the command list is built the supplied discovery handler function needs to have a 'list' option in type. This then gets enumerated, all results returned as None are ignored. - The command executed is then called using it's Execute method. All session management is handled by the command itself. - """ - def __init__(self, browser): - """Initialises class and sets up local variables""" - PlugIn.__init__(self) - DBG_LINE='commands' - self._exported_methods=[] - self._handlers={'':{}} - self._browser = browser - - def plugin(self, owner): - """Makes handlers within the session""" - # Plug into the session and the disco manager - # We only need get and set, results are not needed by a service provider, only a service user. - owner.RegisterHandler('iq',self._CommandHandler,typ='set',ns=NS_COMMANDS) - owner.RegisterHandler('iq',self._CommandHandler,typ='get',ns=NS_COMMANDS) - self._browser.setDiscoHandler(self._DiscoHandler,node=NS_COMMANDS,jid='') - - def plugout(self): - """Removes handlers from the session""" - # unPlug from the session and the disco manager - self._owner.UnregisterHandler('iq',self._CommandHandler,ns=NS_COMMANDS) - for jid in self._handlers: - self._browser.delDiscoHandler(self._DiscoHandler,node=NS_COMMANDS) - - def _CommandHandler(self,conn,request): - """The internal method to process the routing of command execution requests""" - # This is the command handler itself. - # We must: - # Pass on command execution to command handler - # (Do we need to keep session details here, or can that be done in the command?) - jid = str(request.getTo()) - try: - node = request.getTagAttr('command','node') - except: - conn.send(Error(request,ERR_BAD_REQUEST)) - raise NodeProcessed - if self._handlers.has_key(jid): - if self._handlers[jid].has_key(node): - self._handlers[jid][node]['execute'](conn,request) - else: - conn.send(Error(request,ERR_ITEM_NOT_FOUND)) - raise NodeProcessed - elif self._handlers[''].has_key(node): - self._handlers[''][node]['execute'](conn,request) - else: - conn.send(Error(request,ERR_ITEM_NOT_FOUND)) - raise NodeProcessed - - def _DiscoHandler(self,conn,request,typ): - """The internal method to process service discovery requests""" - # This is the disco manager handler. - if typ == 'items': - # We must: - # Generate a list of commands and return the list - # * This handler does not handle individual commands disco requests. - # Pseudo: - # Enumerate the 'item' disco of each command for the specified jid - # Build responce and send - # To make this code easy to write we add an 'list' disco type, it returns a tuple or 'none' if not advertised - list = [] - items = [] - jid = str(request.getTo()) - # Get specific jid based results - if self._handlers.has_key(jid): - for each in self._handlers[jid].keys(): - items.append((jid,each)) - else: - # Get generic results - for each in self._handlers[''].keys(): - items.append(('',each)) - if items != []: - for each in items: - i = self._handlers[each[0]][each[1]]['disco'](conn,request,'list') - if i != None: - list.append(Node(tag='item',attrs={'jid':i[0],'node':i[1],'name':i[2]})) - iq = request.buildReply('result') - if request.getQuerynode(): iq.setQuerynode(request.getQuerynode()) - iq.setQueryPayload(list) - conn.send(iq) - else: - conn.send(Error(request,ERR_ITEM_NOT_FOUND)) - raise NodeProcessed - elif typ == 'info': - return {'ids':[{'category':'automation','type':'command-list'}],'features':[]} - - def addCommand(self,name,cmddisco,cmdexecute,jid=''): - """The method to call if adding a new command to the session, the requred parameters of cmddisco and cmdexecute are the methods to enable that command to be executed""" - # This command takes a command object and the name of the command for registration - # We must: - # Add item into disco - # Add item into command list - if not self._handlers.has_key(jid): - self._handlers[jid]={} - self._browser.setDiscoHandler(self._DiscoHandler,node=NS_COMMANDS,jid=jid) - if self._handlers[jid].has_key(name): - raise NameError,'Command Exists' - else: - self._handlers[jid][name]={'disco':cmddisco,'execute':cmdexecute} - # Need to add disco stuff here - self._browser.setDiscoHandler(cmddisco,node=name,jid=jid) - - def delCommand(self,name,jid=''): - """Removed command from the session""" - # This command takes a command object and the name used for registration - # We must: - # Remove item from disco - # Remove item from command list - if not self._handlers.has_key(jid): - raise NameError,'Jid not found' - if not self._handlers[jid].has_key(name): - raise NameError, 'Command not found' - else: - #Do disco removal here - command = self.getCommand(name,jid)['disco'] - del self._handlers[jid][name] - self._browser.delDiscoHandler(command,node=name,jid=jid) - - def getCommand(self,name,jid=''): - """Returns the command tuple""" - # This gets the command object with name - # We must: - # Return item that matches this name - if not self._handlers.has_key(jid): - raise NameError,'Jid not found' - elif not self._handlers[jid].has_key(name): - raise NameError,'Command not found' - else: - return self._handlers[jid][name] - -class Command_Handler_Prototype(PlugIn): - """This is a prototype command handler, as each command uses a disco method - and execute method you can implement it any way you like, however this is - my first attempt at making a generic handler that you can hang process - stages on too. There is an example command below. - - The parameters are as follows: - name : the name of the command within the jabber environment - description : the natural language description - discofeatures : the features supported by the command - initial : the initial command in the from of {'execute':commandname} - - All stages set the 'actions' dictionary for each session to represent the possible options available. - """ - name = 'examplecommand' - count = 0 - description = 'an example command' - discofeatures = [NS_COMMANDS,NS_DATA] - # This is the command template - def __init__(self,jid=''): - """Set up the class""" - PlugIn.__init__(self) - DBG_LINE='command' - self.sessioncount = 0 - self.sessions = {} - # Disco information for command list pre-formatted as a tuple - self.discoinfo = {'ids':[{'category':'automation','type':'command-node','name':self.description}],'features': self.discofeatures} - self._jid = jid - - def plugin(self,owner): - """Plug command into the commands class""" - # The owner in this instance is the Command Processor - self._commands = owner - self._owner = owner._owner - self._commands.addCommand(self.name,self._DiscoHandler,self.Execute,jid=self._jid) - - def plugout(self): - """Remove command from the commands class""" - self._commands.delCommand(self.name,self._jid) - - def getSessionID(self): - """Returns an id for the command session""" - self.count = self.count+1 - return 'cmd-%s-%d'%(self.name,self.count) - - def Execute(self,conn,request): - """The method that handles all the commands, and routes them to the correct method for that stage.""" - # New request or old? - try: - session = request.getTagAttr('command','sessionid') - except: - session = None - try: - action = request.getTagAttr('command','action') - except: - action = None - if action == None: action = 'execute' - # Check session is in session list - if self.sessions.has_key(session): - if self.sessions[session]['jid']==request.getFrom(): - # Check action is vaild - if self.sessions[session]['actions'].has_key(action): - # Execute next action - self.sessions[session]['actions'][action](conn,request) - else: - # Stage not presented as an option - self._owner.send(Error(request,ERR_BAD_REQUEST)) - raise NodeProcessed - else: - # Jid and session don't match. Go away imposter - self._owner.send(Error(request,ERR_BAD_REQUEST)) - raise NodeProcessed - elif session != None: - # Not on this sessionid you won't. - self._owner.send(Error(request,ERR_BAD_REQUEST)) - raise NodeProcessed - else: - # New session - self.initial[action](conn,request) - - def _DiscoHandler(self,conn,request,type): - """The handler for discovery events""" - if type == 'list': - return (request.getTo(),self.name,self.description) - elif type == 'items': - return [] - elif type == 'info': - return self.discoinfo - -class TestCommand(Command_Handler_Prototype): - """ Example class. You should read source if you wish to understate how it works. - Generally, it presents a "master" that giudes user through to calculate something. - """ - name = 'testcommand' - description = 'a noddy example command' - def __init__(self,jid=''): - """ Init internal constants. """ - Command_Handler_Prototype.__init__(self,jid) - self.initial = {'execute':self.cmdFirstStage} - - def cmdFirstStage(self,conn,request): - """ Determine """ - # This is the only place this should be repeated as all other stages should have SessionIDs - try: - session = request.getTagAttr('command','sessionid') - except: - session = None - if session == None: - session = self.getSessionID() - self.sessions[session]={'jid':request.getFrom(),'actions':{'cancel':self.cmdCancel,'next':self.cmdSecondStage,'execute':self.cmdSecondStage},'data':{'type':None}} - # As this is the first stage we only send a form - reply = request.buildReply('result') - form = DataForm(title='Select type of operation',data=['Use the combobox to select the type of calculation you would like to do, then click Next',DataField(name='calctype',desc='Calculation Type',value=self.sessions[session]['data']['type'],options=[['circlediameter','Calculate the Diameter of a circle'],['circlearea','Calculate the area of a circle']],typ='list-single',required=1)]) - replypayload = [Node('actions',attrs={'execute':'next'},payload=[Node('next')]),form] - reply.addChild(name='command',namespace=NS_COMMANDS,attrs={'node':request.getTagAttr('command','node'),'sessionid':session,'status':'executing'},payload=replypayload) - self._owner.send(reply) - raise NodeProcessed - - def cmdSecondStage(self,conn,request): - form = DataForm(node = request.getTag(name='command').getTag(name='x',namespace=NS_DATA)) - self.sessions[request.getTagAttr('command','sessionid')]['data']['type']=form.getField('calctype').getValue() - self.sessions[request.getTagAttr('command','sessionid')]['actions']={'cancel':self.cmdCancel,None:self.cmdThirdStage,'previous':self.cmdFirstStage,'execute':self.cmdThirdStage,'next':self.cmdThirdStage} - # The form generation is split out to another method as it may be called by cmdThirdStage - self.cmdSecondStageReply(conn,request) - - def cmdSecondStageReply(self,conn,request): - reply = request.buildReply('result') - form = DataForm(title = 'Enter the radius', data=['Enter the radius of the circle (numbers only)',DataField(desc='Radius',name='radius',typ='text-single')]) - replypayload = [Node('actions',attrs={'execute':'complete'},payload=[Node('complete'),Node('prev')]),form] - reply.addChild(name='command',namespace=NS_COMMANDS,attrs={'node':request.getTagAttr('command','node'),'sessionid':request.getTagAttr('command','sessionid'),'status':'executing'},payload=replypayload) - self._owner.send(reply) - raise NodeProcessed - - def cmdThirdStage(self,conn,request): - form = DataForm(node = request.getTag(name='command').getTag(name='x',namespace=NS_DATA)) - try: - num = float(form.getField('radius').getValue()) - except: - self.cmdSecondStageReply(conn,request) - from math import pi - if self.sessions[request.getTagAttr('command','sessionid')]['data']['type'] == 'circlearea': - result = (num**2)*pi - else: - result = num*2*pi - reply = request.buildReply('result') - form = DataForm(typ='result',data=[DataField(desc='result',name='result',value=result)]) - reply.addChild(name='command',namespace=NS_COMMANDS,attrs={'node':request.getTagAttr('command','node'),'sessionid':request.getTagAttr('command','sessionid'),'status':'completed'},payload=[form]) - self._owner.send(reply) - raise NodeProcessed - - def cmdCancel(self,conn,request): - reply = request.buildReply('result') - reply.addChild(name='command',namespace=NS_COMMANDS,attrs={'node':request.getTagAttr('command','node'),'sessionid':request.getTagAttr('command','sessionid'),'status':'cancelled'}) - self._owner.send(reply) - del self.sessions[request.getTagAttr('command','sessionid')] diff --git a/sso/services/jabber/xmpp/debug.py b/sso/services/jabber/xmpp/debug.py deleted file mode 100644 index 34ade88..0000000 --- a/sso/services/jabber/xmpp/debug.py +++ /dev/null @@ -1,423 +0,0 @@ -## debug.py -## -## Copyright (C) 2003 Jacob Lundqvist -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU Lesser General Public License as published -## by the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU Lesser General Public License for more details. - -_version_ = '1.4.0' - -"""\ - -Generic debug class - -Other modules can always define extra debug flags for local usage, as long as -they make sure they append them to debug_flags - -Also its always a good thing to prefix local flags with something, to reduce risk -of coliding flags. Nothing breaks if two flags would be identical, but it might -activate unintended debugging. - -flags can be numeric, but that makes analysing harder, on creation its -not obvious what is activated, and when flag_show is given, output isnt -really meaningfull. - -This Debug class can either be initialized and used on app level, or used independantly -by the individual classes. - -For samples of usage, see samples subdir in distro source, and selftest -in this code - -""" - - - -import sys -import traceback -import time -import os - -import types - -if os.environ.has_key('TERM'): - colors_enabled=True -else: - colors_enabled=False - -color_none = chr(27) + "[0m" -color_black = chr(27) + "[30m" -color_red = chr(27) + "[31m" -color_green = chr(27) + "[32m" -color_brown = chr(27) + "[33m" -color_blue = chr(27) + "[34m" -color_magenta = chr(27) + "[35m" -color_cyan = chr(27) + "[36m" -color_light_gray = chr(27) + "[37m" -color_dark_gray = chr(27) + "[30;1m" -color_bright_red = chr(27) + "[31;1m" -color_bright_green = chr(27) + "[32;1m" -color_yellow = chr(27) + "[33;1m" -color_bright_blue = chr(27) + "[34;1m" -color_purple = chr(27) + "[35;1m" -color_bright_cyan = chr(27) + "[36;1m" -color_white = chr(27) + "[37;1m" - - -""" -Define your flags in yor modules like this: - -from debug import * - -DBG_INIT = 'init' ; debug_flags.append( DBG_INIT ) -DBG_CONNECTION = 'connection' ; debug_flags.append( DBG_CONNECTION ) - - The reason for having a double statement wis so we can validate params - and catch all undefined debug flags - - This gives us control over all used flags, and makes it easier to allow - global debugging in your code, just do something like - - foo = Debug( debug_flags ) - - group flags, that is a flag in it self containing multiple flags should be - defined without the debug_flags.append() sequence, since the parts are already - in the list, also they must of course be defined after the flags they depend on ;) - example: - -DBG_MULTI = [ DBG_INIT, DBG_CONNECTION ] - - - - NoDebug - ------- - To speed code up, typically for product releases or such - use this class instead if you globaly want to disable debugging -""" - - -class NoDebug: - def __init__( self, *args, **kwargs ): - self.debug_flags = [] - def show( self, *args, **kwargs): - pass - def Show( self, *args, **kwargs): - pass - def is_active( self, flag ): - pass - colors={} - def active_set( self, active_flags = None ): - return 0 - - -LINE_FEED = '\n' - - -class Debug: - def __init__( self, - # - # active_flags are those that will trigger output - # - active_flags = None, - # - # Log file should be file object or file namne - # - log_file = sys.stderr, - # - # prefix and sufix can either be set globaly or per call. - # personally I use this to color code debug statements - # with prefix = chr(27) + '[34m' - # sufix = chr(27) + '[37;1m\n' - # - prefix = 'DEBUG: ', - sufix = '\n', - # - # If you want unix style timestamps, - # 0 disables timestamps - # 1 before prefix, good when prefix is a string - # 2 after prefix, good when prefix is a color - # - time_stamp = 0, - # - # flag_show should normaly be of, but can be turned on to get a - # good view of what flags are actually used for calls, - # if it is not None, it should be a string - # flags for current call will be displayed - # with flag_show as separator - # recomended values vould be '-' or ':', but any string goes - # - flag_show = None, - # - # If you dont want to validate flags on each call to - # show(), set this to 0 - # - validate_flags = 1, - # - # If you dont want the welcome message, set to 0 - # default is to show welcome if any flags are active - welcome = -1 - ): - - self.debug_flags = [] - if welcome == -1: - if active_flags and len(active_flags): - welcome = 1 - else: - welcome = 0 - - self._remove_dupe_flags() - if log_file: - if type( log_file ) is type(''): - try: - self._fh = open(log_file,'w') - except: - print 'ERROR: can open %s for writing' - sys.exit(0) - else: ## assume its a stream type object - self._fh = log_file - else: - self._fh = sys.stdout - - if time_stamp not in (0,1,2): - msg2 = '%s' % time_stamp - raise 'Invalid time_stamp param', msg2 - self.prefix = prefix - self.sufix = sufix - self.time_stamp = time_stamp - self.flag_show = None # must be initialised after possible welcome - self.validate_flags = validate_flags - - self.active_set( active_flags ) - if welcome: - self.show('') - caller = sys._getframe(1) # used to get name of caller - try: - mod_name= ":%s" % caller.f_locals['__name__'] - except: - mod_name = "" - self.show('Debug created for %s%s' % (caller.f_code.co_filename, - mod_name )) - self.show(' flags defined: %s' % ','.join( self.active )) - - if type(flag_show) in (type(''), type(None)): - self.flag_show = flag_show - else: - msg2 = '%s' % type(flag_show ) - raise 'Invalid type for flag_show!', msg2 - - - - - - def show( self, msg, flag = None, prefix = None, sufix = None, - lf = 0 ): - """ - flag can be of folowing types: - None - this msg will always be shown if any debugging is on - flag - will be shown if flag is active - (flag1,flag2,,,) - will be shown if any of the given flags - are active - - if prefix / sufix are not given, default ones from init will be used - - lf = -1 means strip linefeed if pressent - lf = 1 means add linefeed if not pressent - """ - - if self.validate_flags: - self._validate_flag( flag ) - - if not self.is_active(flag): - return - if prefix: - pre = prefix - else: - pre = self.prefix - if sufix: - suf = sufix - else: - suf = self.sufix - - if self.time_stamp == 2: - output = '%s%s ' % ( pre, - time.strftime('%b %d %H:%M:%S', - time.localtime(time.time() )), - ) - elif self.time_stamp == 1: - output = '%s %s' % ( time.strftime('%b %d %H:%M:%S', - time.localtime(time.time() )), - pre, - ) - else: - output = pre - - if self.flag_show: - if flag: - output = '%s%s%s' % ( output, flag, self.flag_show ) - else: - # this call uses the global default, - # dont print "None", just show the separator - output = '%s %s' % ( output, self.flag_show ) - - output = '%s%s%s' % ( output, msg, suf ) - if lf: - # strip/add lf if needed - last_char = output[-1] - if lf == 1 and last_char != LINE_FEED: - output = output + LINE_FEED - elif lf == -1 and last_char == LINE_FEED: - output = output[:-1] - try: - self._fh.write( output ) - except: - # unicode strikes again ;) - s=u'' - for i in range(len(output)): - if ord(output[i]) < 128: - c = output[i] - else: - c = '?' - s=s+c - self._fh.write( '%s%s%s' % ( pre, s, suf )) - self._fh.flush() - - - def is_active( self, flag ): - 'If given flag(s) should generate output.' - - # try to abort early to quicken code - if not self.active: - return 0 - if not flag or flag in self.active: - return 1 - else: - # check for multi flag type: - if type( flag ) in ( type(()), type([]) ): - for s in flag: - if s in self.active: - return 1 - return 0 - - - def active_set( self, active_flags = None ): - "returns 1 if any flags where actually set, otherwise 0." - r = 0 - ok_flags = [] - if not active_flags: - #no debuging at all - self.active = [] - elif type( active_flags ) in ( types.TupleType, types.ListType ): - flags = self._as_one_list( active_flags ) - for t in flags: - if t not in self.debug_flags: - sys.stderr.write('Invalid debugflag given: %s\n' % t ) - ok_flags.append( t ) - - self.active = ok_flags - r = 1 - else: - # assume comma string - try: - flags = active_flags.split(',') - except: - self.show( '***' ) - self.show( '*** Invalid debug param given: %s' % active_flags ) - self.show( '*** please correct your param!' ) - self.show( '*** due to this, full debuging is enabled' ) - self.active = self.debug_flags - - for f in flags: - s = f.strip() - ok_flags.append( s ) - self.active = ok_flags - - self._remove_dupe_flags() - return r - - def active_get( self ): - "returns currently active flags." - return self.active - - - def _as_one_list( self, items ): - """ init param might contain nested lists, typically from group flags. - - This code organises lst and remves dupes - """ - if type( items ) <> type( [] ) and type( items ) <> type( () ): - return [ items ] - r = [] - for l in items: - if type( l ) == type([]): - lst2 = self._as_one_list( l ) - for l2 in lst2: - self._append_unique_str(r, l2 ) - elif l == None: - continue - else: - self._append_unique_str(r, l ) - return r - - - def _append_unique_str( self, lst, item ): - """filter out any dupes.""" - if type(item) <> type(''): - msg2 = '%s' % item - raise 'Invalid item type (should be string)',msg2 - if item not in lst: - lst.append( item ) - return lst - - - def _validate_flag( self, flags ): - 'verify that flag is defined.' - if flags: - for f in self._as_one_list( flags ): - if not f in self.debug_flags: - msg2 = '%s' % f - raise 'Invalid debugflag given', msg2 - - def _remove_dupe_flags( self ): - """ - if multiple instances of Debug is used in same app, - some flags might be created multiple time, filter out dupes - """ - unique_flags = [] - for f in self.debug_flags: - if f not in unique_flags: - unique_flags.append(f) - self.debug_flags = unique_flags - - colors={} - def Show(self, flag, msg, prefix=''): - msg=msg.replace('\r','\\r').replace('\n','\\n').replace('><','>\n <') - if not colors_enabled: pass - elif self.colors.has_key(prefix): msg=self.colors[prefix]+msg+color_none - else: msg=color_none+msg - if not colors_enabled: prefixcolor='' - elif self.colors.has_key(flag): prefixcolor=self.colors[flag] - else: prefixcolor=color_none - - if prefix=='error': - _exception = sys.exc_info() - if _exception[0]: - msg=msg+'\n'+''.join(traceback.format_exception(_exception[0], _exception[1], _exception[2])).rstrip() - - prefix= self.prefix+prefixcolor+(flag+' '*12)[:12]+' '+(prefix+' '*6)[:6] - self.show(msg, flag, prefix) - - def is_active( self, flag ): - if not self.active: return 0 - if not flag or flag in self.active and DBG_ALWAYS not in self.active or flag not in self.active and DBG_ALWAYS in self.active : return 1 - return 0 - -DBG_ALWAYS='always' - -##Uncomment this to effectively disable all debugging and all debugging overhead. -#Debug=NoDebug diff --git a/sso/services/jabber/xmpp/dispatcher.py b/sso/services/jabber/xmpp/dispatcher.py deleted file mode 100644 index cc94ee0..0000000 --- a/sso/services/jabber/xmpp/dispatcher.py +++ /dev/null @@ -1,373 +0,0 @@ -## transports.py -## -## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - -# $Id: dispatcher.py,v 1.42 2007/05/18 23:18:36 normanr Exp $ - -""" -Main xmpppy mechanism. Provides library with methods to assign different handlers -to different XMPP stanzas. -Contains one tunable attribute: DefaultTimeout (25 seconds by default). It defines time that -Dispatcher.SendAndWaitForResponce method will wait for reply stanza before giving up. -""" - -import simplexml,time,sys -from protocol import * -from client import PlugIn - -DefaultTimeout=25 -ID=0 - -class Dispatcher(PlugIn): - """ Ancestor of PlugIn class. Handles XMPP stream, i.e. aware of stream headers. - Can be plugged out/in to restart these headers (used for SASL f.e.). """ - def __init__(self): - PlugIn.__init__(self) - DBG_LINE='dispatcher' - self.handlers={} - self._expected={} - self._defaultHandler=None - self._pendingExceptions=[] - self._eventHandler=None - self._cycleHandlers=[] - self._exported_methods=[self.Process,self.RegisterHandler,self.RegisterDefaultHandler,\ - self.RegisterEventHandler,self.UnregisterCycleHandler,self.RegisterCycleHandler,\ - self.RegisterHandlerOnce,self.UnregisterHandler,self.RegisterProtocol,\ - self.WaitForResponse,self.SendAndWaitForResponse,self.send,self.disconnect,\ - self.SendAndCallForResponse, ] - - def dumpHandlers(self): - """ Return set of user-registered callbacks in it's internal format. - Used within the library to carry user handlers set over Dispatcher replugins. """ - return self.handlers - def restoreHandlers(self,handlers): - """ Restores user-registered callbacks structure from dump previously obtained via dumpHandlers. - Used within the library to carry user handlers set over Dispatcher replugins. """ - self.handlers=handlers - - def _init(self): - """ Registers default namespaces/protocols/handlers. Used internally. """ - self.RegisterNamespace('unknown') - self.RegisterNamespace(NS_STREAMS) - self.RegisterNamespace(self._owner.defaultNamespace) - self.RegisterProtocol('iq',Iq) - self.RegisterProtocol('presence',Presence) - self.RegisterProtocol('message',Message) - self.RegisterDefaultHandler(self.returnStanzaHandler) - self.RegisterHandler('error',self.streamErrorHandler,xmlns=NS_STREAMS) - - def plugin(self, owner): - """ Plug the Dispatcher instance into Client class instance and send initial stream header. Used internally.""" - self._init() - for method in self._old_owners_methods: - if method.__name__=='send': self._owner_send=method; break - self._owner.lastErrNode=None - self._owner.lastErr=None - self._owner.lastErrCode=None - self.StreamInit() - - def plugout(self): - """ Prepares instance to be destructed. """ - self.Stream.dispatch=None - self.Stream.DEBUG=None - self.Stream.features=None - self.Stream.destroy() - - def StreamInit(self): - """ Send an initial stream header. """ - self.Stream=simplexml.NodeBuilder() - self.Stream._dispatch_depth=2 - self.Stream.dispatch=self.dispatch - self.Stream.stream_header_received=self._check_stream_start - self._owner.debug_flags.append(simplexml.DBG_NODEBUILDER) - self.Stream.DEBUG=self._owner.DEBUG - self.Stream.features=None - self._metastream=Node('stream:stream') - self._metastream.setNamespace(self._owner.Namespace) - self._metastream.setAttr('version','1.0') - self._metastream.setAttr('xmlns:stream',NS_STREAMS) - self._metastream.setAttr('to',self._owner.Server) - self._owner.send("%s>"%str(self._metastream)[:-2]) - - def _check_stream_start(self,ns,tag,attrs): - if ns<>NS_STREAMS or tag<>'stream': - raise ValueError('Incorrect stream start: (%s,%s). Terminating.'%(tag,ns)) - - def Process(self, timeout=0): - """ Check incoming stream for data waiting. If "timeout" is positive - block for as max. this time. - Returns: - 1) length of processed data if some data were processed; - 2) '0' string if no data were processed but link is alive; - 3) 0 (zero) if underlying connection is closed. - Take note that in case of disconnection detect during Process() call - disconnect handlers are called automatically. - """ - for handler in self._cycleHandlers: handler(self) - if len(self._pendingExceptions) > 0: - _pendingException = self._pendingExceptions.pop() - raise _pendingException[0], _pendingException[1], _pendingException[2] - if self._owner.Connection.pending_data(timeout): - try: data=self._owner.Connection.receive() - except IOError: return - self.Stream.Parse(data) - if len(self._pendingExceptions) > 0: - _pendingException = self._pendingExceptions.pop() - raise _pendingException[0], _pendingException[1], _pendingException[2] - if data: return len(data) - return '0' # It means that nothing is received but link is alive. - - def RegisterNamespace(self,xmlns,order='info'): - """ Creates internal structures for newly registered namespace. - You can register handlers for this namespace afterwards. By default one namespace - already registered (jabber:client or jabber:component:accept depending on context. """ - self.DEBUG('Registering namespace "%s"'%xmlns,order) - self.handlers[xmlns]={} - self.RegisterProtocol('unknown',Protocol,xmlns=xmlns) - self.RegisterProtocol('default',Protocol,xmlns=xmlns) - - def RegisterProtocol(self,tag_name,Proto,xmlns=None,order='info'): - """ Used to declare some top-level stanza name to dispatcher. - Needed to start registering handlers for such stanzas. - Iq, message and presence protocols are registered by default. """ - if not xmlns: xmlns=self._owner.defaultNamespace - self.DEBUG('Registering protocol "%s" as %s(%s)'%(tag_name,Proto,xmlns), order) - self.handlers[xmlns][tag_name]={type:Proto, 'default':[]} - - def RegisterNamespaceHandler(self,xmlns,handler,typ='',ns='', makefirst=0, system=0): - """ Register handler for processing all stanzas for specified namespace. """ - self.RegisterHandler('default', handler, typ, ns, xmlns, makefirst, system) - - def RegisterHandler(self,name,handler,typ='',ns='',xmlns=None, makefirst=0, system=0): - """Register user callback as stanzas handler of declared type. Callback must take - (if chained, see later) arguments: dispatcher instance (for replying), incomed - return of previous handlers. - The callback must raise xmpp.NodeProcessed just before return if it want preven - callbacks to be called with the same stanza as argument _and_, more importantly - library from returning stanza to sender with error set (to be enabled in 0.2 ve - Arguments: - "name" - name of stanza. F.e. "iq". - "handler" - user callback. - "typ" - value of stanza's "type" attribute. If not specified any value match - "ns" - namespace of child that stanza must contain. - "chained" - chain together output of several handlers. - "makefirst" - insert handler in the beginning of handlers list instead of - adding it to the end. Note that more common handlers (i.e. w/o "typ" and " - will be called first nevertheless. - "system" - call handler even if NodeProcessed Exception were raised already. - """ - if not xmlns: xmlns=self._owner.defaultNamespace - self.DEBUG('Registering handler %s for "%s" type->%s ns->%s(%s)'%(handler,name,typ,ns,xmlns), 'info') - if not typ and not ns: typ='default' - if not self.handlers.has_key(xmlns): self.RegisterNamespace(xmlns,'warn') - if not self.handlers[xmlns].has_key(name): self.RegisterProtocol(name,Protocol,xmlns,'warn') - if not self.handlers[xmlns][name].has_key(typ+ns): self.handlers[xmlns][name][typ+ns]=[] - if makefirst: self.handlers[xmlns][name][typ+ns].insert(0,{'func':handler,'system':system}) - else: self.handlers[xmlns][name][typ+ns].append({'func':handler,'system':system}) - - def RegisterHandlerOnce(self,name,handler,typ='',ns='',xmlns=None,makefirst=0, system=0): - """ Unregister handler after first call (not implemented yet). """ - if not xmlns: xmlns=self._owner.defaultNamespace - self.RegisterHandler(name, handler, typ, ns, xmlns, makefirst, system) - - def UnregisterHandler(self,name,handler,typ='',ns='',xmlns=None): - """ Unregister handler. "typ" and "ns" must be specified exactly the same as with registering.""" - if not xmlns: xmlns=self._owner.defaultNamespace - if not self.handlers.has_key(xmlns): return - if not typ and not ns: typ='default' - for pack in self.handlers[xmlns][name][typ+ns]: - if handler==pack['func']: break - else: pack=None - try: self.handlers[xmlns][name][typ+ns].remove(pack) - except ValueError: pass - - def RegisterDefaultHandler(self,handler): - """ Specify the handler that will be used if no NodeProcessed exception were raised. - This is returnStanzaHandler by default. """ - self._defaultHandler=handler - - def RegisterEventHandler(self,handler): - """ Register handler that will process events. F.e. "FILERECEIVED" event. """ - self._eventHandler=handler - - def returnStanzaHandler(self,conn,stanza): - """ Return stanza back to the sender with error set. """ - if stanza.getType() in ['get','set']: - conn.send(Error(stanza,ERR_FEATURE_NOT_IMPLEMENTED)) - - def streamErrorHandler(self,conn,error): - name,text='error',error.getData() - for tag in error.getChildren(): - if tag.getNamespace()==NS_XMPP_STREAMS: - if tag.getName()=='text': text=tag.getData() - else: name=tag.getName() - if name in stream_exceptions.keys(): exc=stream_exceptions[name] - else: exc=StreamError - raise exc((name,text)) - - def RegisterCycleHandler(self,handler): - """ Register handler that will be called on every Dispatcher.Process() call. """ - if handler not in self._cycleHandlers: self._cycleHandlers.append(handler) - - def UnregisterCycleHandler(self,handler): - """ Unregister handler that will is called on every Dispatcher.Process() call.""" - if handler in self._cycleHandlers: self._cycleHandlers.remove(handler) - - def Event(self,realm,event,data): - """ Raise some event. Takes three arguments: - 1) "realm" - scope of event. Usually a namespace. - 2) "event" - the event itself. F.e. "SUCESSFULL SEND". - 3) data that comes along with event. Depends on event.""" - if self._eventHandler: self._eventHandler(realm,event,data) - - def dispatch(self,stanza,session=None,direct=0): - """ Main procedure that performs XMPP stanza recognition and calling apppropriate handlers for it. - Called internally. """ - if not session: session=self - session.Stream._mini_dom=None - name=stanza.getName() - - if not direct and self._owner._route: - if name == 'route': - if stanza.getAttr('error') == None: - if len(stanza.getChildren()) == 1: - stanza = stanza.getChildren()[0] - name=stanza.getName() - else: - for each in stanza.getChildren(): - self.dispatch(each,session,direct=1) - return - elif name == 'presence': - return - elif name in ('features','bind'): - pass - else: - raise UnsupportedStanzaType(name) - - if name=='features': session.Stream.features=stanza - - xmlns=stanza.getNamespace() - if not self.handlers.has_key(xmlns): - self.DEBUG("Unknown namespace: " + xmlns,'warn') - xmlns='unknown' - if not self.handlers[xmlns].has_key(name): - self.DEBUG("Unknown stanza: " + name,'warn') - name='unknown' - else: - self.DEBUG("Got %s/%s stanza"%(xmlns,name), 'ok') - - if stanza.__class__.__name__=='Node': stanza=self.handlers[xmlns][name][type](node=stanza) - - typ=stanza.getType() - if not typ: typ='' - stanza.props=stanza.getProperties() - ID=stanza.getID() - - session.DEBUG("Dispatching %s stanza with type->%s props->%s id->%s"%(name,typ,stanza.props,ID),'ok') - - list=['default'] # we will use all handlers: - if self.handlers[xmlns][name].has_key(typ): list.append(typ) # from very common... - for prop in stanza.props: - if self.handlers[xmlns][name].has_key(prop): list.append(prop) - if typ and self.handlers[xmlns][name].has_key(typ+prop): list.append(typ+prop) # ...to very particular - - chain=self.handlers[xmlns]['default']['default'] - for key in list: - if key: chain = chain + self.handlers[xmlns][name][key] - - output='' - if session._expected.has_key(ID): - user=0 - if type(session._expected[ID])==type(()): - cb,args=session._expected[ID] - session.DEBUG("Expected stanza arrived. Callback %s(%s) found!"%(cb,args),'ok') - try: cb(session,stanza,**args) - except Exception, typ: - if typ.__class__.__name__<>'NodeProcessed': raise - else: - session.DEBUG("Expected stanza arrived!",'ok') - session._expected[ID]=stanza - else: user=1 - for handler in chain: - if user or handler['system']: - try: - handler['func'](session,stanza) - except Exception, typ: - if typ.__class__.__name__<>'NodeProcessed': - self._pendingExceptions.insert(0, sys.exc_info()) - return - user=0 - if user and self._defaultHandler: self._defaultHandler(session,stanza) - - def WaitForResponse(self, ID, timeout=DefaultTimeout): - """ Block and wait until stanza with specific "id" attribute will come. - If no such stanza is arrived within timeout, return None. - If operation failed for some reason then owner's attributes - lastErrNode, lastErr and lastErrCode are set accordingly. """ - self._expected[ID]=None - has_timed_out=0 - abort_time=time.time() + timeout - self.DEBUG("Waiting for ID:%s with timeout %s..." % (ID,timeout),'wait') - while not self._expected[ID]: - if not self.Process(0.04): - self._owner.lastErr="Disconnect" - return None - if time.time() > abort_time: - self._owner.lastErr="Timeout" - return None - response=self._expected[ID] - del self._expected[ID] - if response.getErrorCode(): - self._owner.lastErrNode=response - self._owner.lastErr=response.getError() - self._owner.lastErrCode=response.getErrorCode() - return response - - def SendAndWaitForResponse(self, stanza, timeout=DefaultTimeout): - """ Put stanza on the wire and wait for recipient's response to it. """ - return self.WaitForResponse(self.send(stanza),timeout) - - def SendAndCallForResponse(self, stanza, func, args={}): - """ Put stanza on the wire and call back when recipient replies. - Additional callback arguments can be specified in args. """ - self._expected[self.send(stanza)]=(func,args) - - def send(self,stanza): - """ Serialise stanza and put it on the wire. Assign an unique ID to it before send. - Returns assigned ID.""" - if type(stanza) in [type(''), type(u'')]: return self._owner_send(stanza) - if not isinstance(stanza,Protocol): _ID=None - elif not stanza.getID(): - global ID - ID+=1 - _ID=`ID` - stanza.setID(_ID) - else: _ID=stanza.getID() - if self._owner._registered_name and not stanza.getAttr('from'): stanza.setAttr('from',self._owner._registered_name) - if self._owner._route and stanza.getName()!='bind': - to=self._owner.Server - if stanza.getTo() and stanza.getTo().getDomain(): - to=stanza.getTo().getDomain() - frm=stanza.getFrom() - if frm.getDomain(): - frm=frm.getDomain() - route=Protocol('route',to=to,frm=frm,payload=[stanza]) - stanza=route - stanza.setNamespace(self._owner.Namespace) - stanza.setParent(self._metastream) - self._owner_send(stanza) - return _ID - - def disconnect(self): - """ Send a stream terminator and and handle all incoming stanzas before stream closure. """ - self._owner_send('') - while self.Process(1): pass diff --git a/sso/services/jabber/xmpp/features.py b/sso/services/jabber/xmpp/features.py deleted file mode 100644 index c7993c2..0000000 --- a/sso/services/jabber/xmpp/features.py +++ /dev/null @@ -1,182 +0,0 @@ -## features.py -## -## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - -# $Id: features.py,v 1.25 2009/04/07 07:11:48 snakeru Exp $ - -""" -This module contains variable stuff that is not worth splitting into separate modules. -Here is: - DISCO client and agents-to-DISCO and browse-to-DISCO emulators. - IBR and password manager. - jabber:iq:privacy methods -All these methods takes 'disp' first argument that should be already connected -(and in most cases already authorised) dispatcher instance. -""" - -from protocol import * - -REGISTER_DATA_RECEIVED='REGISTER DATA RECEIVED' - -### DISCO ### http://jabber.org/protocol/disco ### JEP-0030 #################### -### Browse ### jabber:iq:browse ### JEP-0030 ################################### -### Agents ### jabber:iq:agents ### JEP-0030 ################################### -def _discover(disp,ns,jid,node=None,fb2b=0,fb2a=1): - """ Try to obtain info from the remote object. - If remote object doesn't support disco fall back to browse (if fb2b is true) - and if it doesnt support browse (or fb2b is not true) fall back to agents protocol - (if gb2a is true). Returns obtained info. Used internally. """ - iq=Iq(to=jid,typ='get',queryNS=ns) - if node: iq.setQuerynode(node) - rep=disp.SendAndWaitForResponse(iq) - if fb2b and not isResultNode(rep): rep=disp.SendAndWaitForResponse(Iq(to=jid,typ='get',queryNS=NS_BROWSE)) # Fallback to browse - if fb2a and not isResultNode(rep): rep=disp.SendAndWaitForResponse(Iq(to=jid,typ='get',queryNS=NS_AGENTS)) # Fallback to agents - if isResultNode(rep): return [n for n in rep.getQueryPayload() if isinstance(n, Node)] - return [] - -def discoverItems(disp,jid,node=None): - """ Query remote object about any items that it contains. Return items list. """ - """ According to JEP-0030: - query MAY have node attribute - item: MUST HAVE jid attribute and MAY HAVE name, node, action attributes. - action attribute of item can be either of remove or update value.""" - ret=[] - for i in _discover(disp,NS_DISCO_ITEMS,jid,node): - if i.getName()=='agent' and i.getTag('name'): i.setAttr('name',i.getTagData('name')) - ret.append(i.attrs) - return ret - -def discoverInfo(disp,jid,node=None): - """ Query remote object about info that it publishes. Returns identities and features lists.""" - """ According to JEP-0030: - query MAY have node attribute - identity: MUST HAVE category and name attributes and MAY HAVE type attribute. - feature: MUST HAVE var attribute""" - identities , features = [] , [] - for i in _discover(disp,NS_DISCO_INFO,jid,node): - if i.getName()=='identity': identities.append(i.attrs) - elif i.getName()=='feature': features.append(i.getAttr('var')) - elif i.getName()=='agent': - if i.getTag('name'): i.setAttr('name',i.getTagData('name')) - if i.getTag('description'): i.setAttr('name',i.getTagData('description')) - identities.append(i.attrs) - if i.getTag('groupchat'): features.append(NS_GROUPCHAT) - if i.getTag('register'): features.append(NS_REGISTER) - if i.getTag('search'): features.append(NS_SEARCH) - return identities , features - -### Registration ### jabber:iq:register ### JEP-0077 ########################### -def getRegInfo(disp,host,info={},sync=True): - """ Gets registration form from remote host. - You can pre-fill the info dictionary. - F.e. if you are requesting info on registering user joey than specify - info as {'username':'joey'}. See JEP-0077 for details. - 'disp' must be connected dispatcher instance.""" - iq=Iq('get',NS_REGISTER,to=host) - for i in info.keys(): iq.setTagData(i,info[i]) - if sync: - resp=disp.SendAndWaitForResponse(iq) - _ReceivedRegInfo(disp.Dispatcher,resp, host) - return resp - else: disp.SendAndCallForResponse(iq,_ReceivedRegInfo, {'agent': host}) - -def _ReceivedRegInfo(con, resp, agent): - iq=Iq('get',NS_REGISTER,to=agent) - if not isResultNode(resp): return - df=resp.getTag('query',namespace=NS_REGISTER).getTag('x',namespace=NS_DATA) - if df: - con.Event(NS_REGISTER,REGISTER_DATA_RECEIVED,(agent, DataForm(node=df))) - return - df=DataForm(typ='form') - for i in resp.getQueryPayload(): - if type(i)<>type(iq): pass - elif i.getName()=='instructions': df.addInstructions(i.getData()) - else: df.setField(i.getName()).setValue(i.getData()) - con.Event(NS_REGISTER,REGISTER_DATA_RECEIVED,(agent, df)) - -def register(disp,host,info): - """ Perform registration on remote server with provided info. - disp must be connected dispatcher instance. - Returns true or false depending on registration result. - If registration fails you can get additional info from the dispatcher's owner - attributes lastErrNode, lastErr and lastErrCode. - """ - iq=Iq('set',NS_REGISTER,to=host) - if type(info)<>type({}): info=info.asDict() - for i in info.keys(): iq.setTag('query').setTagData(i,info[i]) - resp=disp.SendAndWaitForResponse(iq) - if isResultNode(resp): return 1 - -def unregister(disp,host): - """ Unregisters with host (permanently removes account). - disp must be connected and authorized dispatcher instance. - Returns true on success.""" - resp=disp.SendAndWaitForResponse(Iq('set',NS_REGISTER,to=host,payload=[Node('remove')])) - if isResultNode(resp): return 1 - -def changePasswordTo(disp,newpassword,host=None): - """ Changes password on specified or current (if not specified) server. - disp must be connected and authorized dispatcher instance. - Returns true on success.""" - if not host: host=disp._owner.Server - resp=disp.SendAndWaitForResponse(Iq('set',NS_REGISTER,to=host,payload=[Node('username',payload=[disp._owner.Server]),Node('password',payload=[newpassword])])) - if isResultNode(resp): return 1 - -### Privacy ### jabber:iq:privacy ### draft-ietf-xmpp-im-19 #################### -#type=[jid|group|subscription] -#action=[allow|deny] - -def getPrivacyLists(disp): - """ Requests privacy lists from connected server. - Returns dictionary of existing lists on success.""" - try: - dict={'lists':[]} - resp=disp.SendAndWaitForResponse(Iq('get',NS_PRIVACY)) - if not isResultNode(resp): return - for list in resp.getQueryPayload(): - if list.getName()=='list': dict['lists'].append(list.getAttr('name')) - else: dict[list.getName()]=list.getAttr('name') - return dict - except: pass - -def getPrivacyList(disp,listname): - """ Requests specific privacy list listname. Returns list of XML nodes (rules) - taken from the server responce.""" - try: - resp=disp.SendAndWaitForResponse(Iq('get',NS_PRIVACY,payload=[Node('list',{'name':listname})])) - if isResultNode(resp): return resp.getQueryPayload()[0] - except: pass - -def setActivePrivacyList(disp,listname=None,typ='active'): - """ Switches privacy list 'listname' to specified type. - By default the type is 'active'. Returns true on success.""" - if listname: attrs={'name':listname} - else: attrs={} - resp=disp.SendAndWaitForResponse(Iq('set',NS_PRIVACY,payload=[Node(typ,attrs)])) - if isResultNode(resp): return 1 - -def setDefaultPrivacyList(disp,listname=None): - """ Sets the default privacy list as 'listname'. Returns true on success.""" - return setActivePrivacyList(disp,listname,'default') - -def setPrivacyList(disp,list): - """ Set the ruleset. 'list' should be the simpleXML node formatted - according to RFC 3921 (XMPP-IM) (I.e. Node('list',{'name':listname},payload=[...]) ) - Returns true on success.""" - resp=disp.SendAndWaitForResponse(Iq('set',NS_PRIVACY,payload=[list])) - if isResultNode(resp): return 1 - -def delPrivacyList(disp,listname): - """ Deletes privacy list 'listname'. Returns true on success.""" - resp=disp.SendAndWaitForResponse(Iq('set',NS_PRIVACY,payload=[Node('list',{'name':listname})])) - if isResultNode(resp): return 1 diff --git a/sso/services/jabber/xmpp/filetransfer.py b/sso/services/jabber/xmpp/filetransfer.py deleted file mode 100644 index 87ddc21..0000000 --- a/sso/services/jabber/xmpp/filetransfer.py +++ /dev/null @@ -1,199 +0,0 @@ -## filetransfer.py -## -## Copyright (C) 2004 Alexey "Snake" Nezhdanov -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - -# $Id: filetransfer.py,v 1.6 2004/12/25 20:06:59 snakeru Exp $ - -""" -This module contains IBB class that is the simple implementation of JEP-0047. -Note that this is just a transport for data. You have to negotiate data transfer before -(via StreamInitiation most probably). Unfortunately SI is not implemented yet. -""" - -from protocol import * -from dispatcher import PlugIn -import base64 - -class IBB(PlugIn): - """ IBB used to transfer small-sized data chunk over estabilished xmpp connection. - Data is split into small blocks (by default 3000 bytes each), encoded as base 64 - and sent to another entity that compiles these blocks back into the data chunk. - This is very inefficiend but should work under any circumstances. Note that - using IBB normally should be the last resort. - """ - def __init__(self): - """ Initialise internal variables. """ - PlugIn.__init__(self) - self.DBG_LINE='ibb' - self._exported_methods=[self.OpenStream] - self._streams={} - self._ampnode=Node(NS_AMP+' amp',payload=[Node('rule',{'condition':'deliver-at','value':'stored','action':'error'}),Node('rule',{'condition':'match-resource','value':'exact','action':'error'})]) - - def plugin(self,owner): - """ Register handlers for receiving incoming datastreams. Used internally. """ - self._owner.RegisterHandlerOnce('iq',self.StreamOpenReplyHandler) # Move to StreamOpen and specify stanza id - self._owner.RegisterHandler('iq',self.IqHandler,ns=NS_IBB) - self._owner.RegisterHandler('message',self.ReceiveHandler,ns=NS_IBB) - - def IqHandler(self,conn,stanza): - """ Handles streams state change. Used internally. """ - typ=stanza.getType() - self.DEBUG('IqHandler called typ->%s'%typ,'info') - if typ=='set' and stanza.getTag('open',namespace=NS_IBB): self.StreamOpenHandler(conn,stanza) - elif typ=='set' and stanza.getTag('close',namespace=NS_IBB): self.StreamCloseHandler(conn,stanza) - elif typ=='result': self.StreamCommitHandler(conn,stanza) - elif typ=='error': self.StreamOpenReplyHandler(conn,stanza) - else: conn.send(Error(stanza,ERR_BAD_REQUEST)) - raise NodeProcessed - - def StreamOpenHandler(self,conn,stanza): - """ Handles opening of new incoming stream. Used internally. """ - """ - - - -""" - err=None - sid,blocksize=stanza.getTagAttr('open','sid'),stanza.getTagAttr('open','block-size') - self.DEBUG('StreamOpenHandler called sid->%s blocksize->%s'%(sid,blocksize),'info') - try: blocksize=int(blocksize) - except: err=ERR_BAD_REQUEST - if not sid or not blocksize: err=ERR_BAD_REQUEST - elif sid in self._streams.keys(): err=ERR_UNEXPECTED_REQUEST - if err: rep=Error(stanza,err) - else: - self.DEBUG("Opening stream: id %s, block-size %s"%(sid,blocksize),'info') - rep=Protocol('iq',stanza.getFrom(),'result',stanza.getTo(),{'id':stanza.getID()}) - self._streams[sid]={'direction':'<'+str(stanza.getFrom()),'block-size':blocksize,'fp':open('/tmp/xmpp_file_'+sid,'w'),'seq':0,'syn_id':stanza.getID()} - conn.send(rep) - - def OpenStream(self,sid,to,fp,blocksize=3000): - """ Start new stream. You should provide stream id 'sid', the endpoind jid 'to', - the file object containing info for send 'fp'. Also the desired blocksize can be specified. - Take into account that recommended stanza size is 4k and IBB uses base64 encoding - that increases size of data by 1/3.""" - if sid in self._streams.keys(): return - if not JID(to).getResource(): return - self._streams[sid]={'direction':'|>'+to,'block-size':blocksize,'fp':fp,'seq':0} - self._owner.RegisterCycleHandler(self.SendHandler) - syn=Protocol('iq',to,'set',payload=[Node(NS_IBB+' open',{'sid':sid,'block-size':blocksize})]) - self._owner.send(syn) - self._streams[sid]['syn_id']=syn.getID() - return self._streams[sid] - - def SendHandler(self,conn): - """ Send next portion of data if it is time to do it. Used internally. """ - self.DEBUG('SendHandler called','info') - for sid in self._streams.keys(): - stream=self._streams[sid] - if stream['direction'][:2]=='|>': cont=1 - elif stream['direction'][0]=='>': - chunk=stream['fp'].read(stream['block-size']) - if chunk: - datanode=Node(NS_IBB+' data',{'sid':sid,'seq':stream['seq']},base64.encodestring(chunk)) - stream['seq']+=1 - if stream['seq']==65536: stream['seq']=0 - conn.send(Protocol('message',stream['direction'][1:],payload=[datanode,self._ampnode])) - else: - """ notify the other side about stream closing - notify the local user about sucessfull send - delete the local stream""" - conn.send(Protocol('iq',stream['direction'][1:],'set',payload=[Node(NS_IBB+' close',{'sid':sid})])) - conn.Event(self.DBG_LINE,'SUCCESSFULL SEND',stream) - del self._streams[sid] - self._owner.UnregisterCycleHandler(self.SendHandler) - - """ - - - qANQR1DBwU4DX7jmYZnncmUQB/9KuKBddzQH+tZ1ZywKK0yHKnq57kWq+RFtQdCJ - WpdWpR0uQsuJe7+vh3NWn59/gTc5MDlX8dS9p0ovStmNcyLhxVgmqS8ZKhsblVeu - IpQ0JgavABqibJolc3BKrVtVV1igKiX/N7Pi8RtY1K18toaMDhdEfhBRzO/XB0+P - AQhYlRjNacGcslkhXqNjK5Va4tuOAPy2n1Q8UUrHbUd0g+xJ9Bm0G0LZXyvCWyKH - kuNEHFQiLuCY6Iv0myq6iX6tjuHehZlFSh80b5BVV9tNLwNR5Eqz1klxMhoghJOA - - - - - - -""" - - def ReceiveHandler(self,conn,stanza): - """ Receive next portion of incoming datastream and store it write - it to temporary file. Used internally. - """ - sid,seq,data=stanza.getTagAttr('data','sid'),stanza.getTagAttr('data','seq'),stanza.getTagData('data') - self.DEBUG('ReceiveHandler called sid->%s seq->%s'%(sid,seq),'info') - try: seq=int(seq); data=base64.decodestring(data) - except: seq=''; data='' - err=None - if not sid in self._streams.keys(): err=ERR_ITEM_NOT_FOUND - else: - stream=self._streams[sid] - if not data: err=ERR_BAD_REQUEST - elif seq<>stream['seq']: err=ERR_UNEXPECTED_REQUEST - else: - self.DEBUG('Successfull receive sid->%s %s+%s bytes'%(sid,stream['fp'].tell(),len(data)),'ok') - stream['seq']+=1 - stream['fp'].write(data) - if err: - self.DEBUG('Error on receive: %s'%err,'error') - conn.send(Error(Iq(to=stanza.getFrom(),frm=stanza.getTo(),payload=[Node(NS_IBB+' close')]),err,reply=0)) - - def StreamCloseHandler(self,conn,stanza): - """ Handle stream closure due to all data transmitted. - Raise xmpppy event specifying successfull data receive. """ - sid=stanza.getTagAttr('close','sid') - self.DEBUG('StreamCloseHandler called sid->%s'%sid,'info') - if sid in self._streams.keys(): - conn.send(stanza.buildReply('result')) - conn.Event(self.DBG_LINE,'SUCCESSFULL RECEIVE',self._streams[sid]) - del self._streams[sid] - else: conn.send(Error(stanza,ERR_ITEM_NOT_FOUND)) - - def StreamBrokenHandler(self,conn,stanza): - """ Handle stream closure due to all some error while receiving data. - Raise xmpppy event specifying unsuccessfull data receive. """ - syn_id=stanza.getID() - self.DEBUG('StreamBrokenHandler called syn_id->%s'%syn_id,'info') - for sid in self._streams.keys(): - stream=self._streams[sid] - if stream['syn_id']==syn_id: - if stream['direction'][0]=='<': conn.Event(self.DBG_LINE,'ERROR ON RECEIVE',stream) - else: conn.Event(self.DBG_LINE,'ERROR ON SEND',stream) - del self._streams[sid] - - def StreamOpenReplyHandler(self,conn,stanza): - """ Handle remote side reply about is it agree or not to receive our datastream. - Used internally. Raises xmpppy event specfiying if the data transfer - is agreed upon.""" - syn_id=stanza.getID() - self.DEBUG('StreamOpenReplyHandler called syn_id->%s'%syn_id,'info') - for sid in self._streams.keys(): - stream=self._streams[sid] - if stream['syn_id']==syn_id: - if stanza.getType()=='error': - if stream['direction'][0]=='<': conn.Event(self.DBG_LINE,'ERROR ON RECEIVE',stream) - else: conn.Event(self.DBG_LINE,'ERROR ON SEND',stream) - del self._streams[sid] - elif stanza.getType()=='result': - if stream['direction'][0]=='|': - stream['direction']=stream['direction'][1:] - conn.Event(self.DBG_LINE,'STREAM COMMITTED',stream) - else: conn.send(Error(stanza,ERR_UNEXPECTED_REQUEST)) diff --git a/sso/services/jabber/xmpp/jep0106.py b/sso/services/jabber/xmpp/jep0106.py deleted file mode 100644 index fcf1114..0000000 --- a/sso/services/jabber/xmpp/jep0106.py +++ /dev/null @@ -1,57 +0,0 @@ - -# JID Escaping XEP-0106 for the xmpppy based transports written by Norman Rasmussen - -"""This file is the XEP-0106 commands. - -Implemented commands as follows: - -4.2 Encode : Encoding Transformation -4.3 Decode : Decoding Transformation - - -""" - -xep0106mapping = [ - [' ' ,'20'], - ['"' ,'22'], - ['&' ,'26'], - ['\'','27'], - ['/' ,'2f'], - [':' ,'3a'], - ['<' ,'3c'], - ['>' ,'3e'], - ['@' ,'40']] - -def JIDEncode(str): - str = str.replace('\\5c', '\\5c5c') - for each in xep0106mapping: - str = str.replace('\\' + each[1], '\\5c' + each[1]) - for each in xep0106mapping: - str = str.replace(each[0], '\\' + each[1]) - return str - -def JIDDecode(str): - for each in xep0106mapping: - str = str.replace('\\' + each[1], each[0]) - return str.replace('\\5c', '\\') - -if __name__ == "__main__": - def test(before,valid): - during = JIDEncode(before) - after = JIDDecode(during) - if during == valid and after == before: - print 'PASS Before: ' + before - print 'PASS During: ' + during - else: - print 'FAIL Before: ' + before - print 'FAIL During: ' + during - print 'FAIL After : ' + after - print - - test('jid escaping',r'jid\20escaping') - test(r'\3and\2is\5@example.com',r'\5c3and\2is\5\40example.com') - test(r'\3catsand\2catsis\5cats@example.com',r'\5c3catsand\2catsis\5c5cats\40example.com') - test(r'\2plus\2is\4',r'\2plus\2is\4') - test(r'foo\bar',r'foo\bar') - test(r'foob\41r',r'foob\41r') - test('here\'s_a wild_&_/cr%zy/_address@example.com',r'here\27s_a\20wild_\26_\2fcr%zy\2f_address\40example.com') diff --git a/sso/services/jabber/xmpp/protocol.py b/sso/services/jabber/xmpp/protocol.py deleted file mode 100644 index 3e49b8d..0000000 --- a/sso/services/jabber/xmpp/protocol.py +++ /dev/null @@ -1,860 +0,0 @@ -## protocol.py -## -## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - -# $Id: protocol.py,v 1.60 2009/04/07 11:14:28 snakeru Exp $ - -""" -Protocol module contains tools that is needed for processing of -xmpp-related data structures. -""" - -from simplexml import Node,ustr -import time -NS_ACTIVITY ='http://jabber.org/protocol/activity' # XEP-0108 -NS_ADDRESS ='http://jabber.org/protocol/address' # XEP-0033 -NS_ADMIN ='http://jabber.org/protocol/admin' # XEP-0133 -NS_ADMIN_ADD_USER =NS_ADMIN+'#add-user' # XEP-0133 -NS_ADMIN_DELETE_USER =NS_ADMIN+'#delete-user' # XEP-0133 -NS_ADMIN_DISABLE_USER =NS_ADMIN+'#disable-user' # XEP-0133 -NS_ADMIN_REENABLE_USER =NS_ADMIN+'#reenable-user' # XEP-0133 -NS_ADMIN_END_USER_SESSION =NS_ADMIN+'#end-user-session' # XEP-0133 -NS_ADMIN_GET_USER_PASSWORD =NS_ADMIN+'#get-user-password' # XEP-0133 -NS_ADMIN_CHANGE_USER_PASSWORD =NS_ADMIN+'#change-user-password' # XEP-0133 -NS_ADMIN_GET_USER_ROSTER =NS_ADMIN+'#get-user-roster' # XEP-0133 -NS_ADMIN_GET_USER_LASTLOGIN =NS_ADMIN+'#get-user-lastlogin' # XEP-0133 -NS_ADMIN_USER_STATS =NS_ADMIN+'#user-stats' # XEP-0133 -NS_ADMIN_EDIT_BLACKLIST =NS_ADMIN+'#edit-blacklist' # XEP-0133 -NS_ADMIN_EDIT_WHITELIST =NS_ADMIN+'#edit-whitelist' # XEP-0133 -NS_ADMIN_REGISTERED_USERS_NUM =NS_ADMIN+'#get-registered-users-num' # XEP-0133 -NS_ADMIN_DISABLED_USERS_NUM =NS_ADMIN+'#get-disabled-users-num' # XEP-0133 -NS_ADMIN_ONLINE_USERS_NUM =NS_ADMIN+'#get-online-users-num' # XEP-0133 -NS_ADMIN_ACTIVE_USERS_NUM =NS_ADMIN+'#get-active-users-num' # XEP-0133 -NS_ADMIN_IDLE_USERS_NUM =NS_ADMIN+'#get-idle-users-num' # XEP-0133 -NS_ADMIN_REGISTERED_USERS_LIST =NS_ADMIN+'#get-registered-users-list' # XEP-0133 -NS_ADMIN_DISABLED_USERS_LIST =NS_ADMIN+'#get-disabled-users-list' # XEP-0133 -NS_ADMIN_ONLINE_USERS_LIST =NS_ADMIN+'#get-online-users-list' # XEP-0133 -NS_ADMIN_ACTIVE_USERS_LIST =NS_ADMIN+'#get-active-users-list' # XEP-0133 -NS_ADMIN_IDLE_USERS_LIST =NS_ADMIN+'#get-idle-users-list' # XEP-0133 -NS_ADMIN_ANNOUNCE =NS_ADMIN+'#announce' # XEP-0133 -NS_ADMIN_SET_MOTD =NS_ADMIN+'#set-motd' # XEP-0133 -NS_ADMIN_EDIT_MOTD =NS_ADMIN+'#edit-motd' # XEP-0133 -NS_ADMIN_DELETE_MOTD =NS_ADMIN+'#delete-motd' # XEP-0133 -NS_ADMIN_SET_WELCOME =NS_ADMIN+'#set-welcome' # XEP-0133 -NS_ADMIN_DELETE_WELCOME =NS_ADMIN+'#delete-welcome' # XEP-0133 -NS_ADMIN_EDIT_ADMIN =NS_ADMIN+'#edit-admin' # XEP-0133 -NS_ADMIN_RESTART =NS_ADMIN+'#restart' # XEP-0133 -NS_ADMIN_SHUTDOWN =NS_ADMIN+'#shutdown' # XEP-0133 -NS_AGENTS ='jabber:iq:agents' # XEP-0094 (historical) -NS_AMP ='http://jabber.org/protocol/amp' # XEP-0079 -NS_AMP_ERRORS =NS_AMP+'#errors' # XEP-0079 -NS_AUTH ='jabber:iq:auth' # XEP-0078 -NS_AVATAR ='jabber:iq:avatar' # XEP-0008 (historical) -NS_BIND ='urn:ietf:params:xml:ns:xmpp-bind' # RFC 3920 -NS_BROWSE ='jabber:iq:browse' # XEP-0011 (historical) -NS_BYTESTREAM ='http://jabber.org/protocol/bytestreams' # XEP-0065 -NS_CAPS ='http://jabber.org/protocol/caps' # XEP-0115 -NS_CHATSTATES ='http://jabber.org/protocol/chatstates' # XEP-0085 -NS_CLIENT ='jabber:client' # RFC 3921 -NS_COMMANDS ='http://jabber.org/protocol/commands' # XEP-0050 -NS_COMPONENT_ACCEPT ='jabber:component:accept' # XEP-0114 -NS_COMPONENT_1 ='http://jabberd.jabberstudio.org/ns/component/1.0' # Jabberd2 -NS_COMPRESS ='http://jabber.org/protocol/compress' # XEP-0138 -NS_DATA ='jabber:x:data' # XEP-0004 -NS_DATA_LAYOUT ='http://jabber.org/protocol/xdata-layout' # XEP-0141 -NS_DATA_VALIDATE ='http://jabber.org/protocol/xdata-validate' # XEP-0122 -NS_DELAY ='jabber:x:delay' # XEP-0091 (deprecated) -NS_DIALBACK ='jabber:server:dialback' # RFC 3921 -NS_DISCO ='http://jabber.org/protocol/disco' # XEP-0030 -NS_DISCO_INFO =NS_DISCO+'#info' # XEP-0030 -NS_DISCO_ITEMS =NS_DISCO+'#items' # XEP-0030 -NS_ENCRYPTED ='jabber:x:encrypted' # XEP-0027 -NS_EVENT ='jabber:x:event' # XEP-0022 (deprecated) -NS_FEATURE ='http://jabber.org/protocol/feature-neg' # XEP-0020 -NS_FILE ='http://jabber.org/protocol/si/profile/file-transfer' # XEP-0096 -NS_GATEWAY ='jabber:iq:gateway' # XEP-0100 -NS_GEOLOC ='http://jabber.org/protocol/geoloc' # XEP-0080 -NS_GROUPCHAT ='gc-1.0' # XEP-0045 -NS_HTTP_BIND ='http://jabber.org/protocol/httpbind' # XEP-0124 -NS_IBB ='http://jabber.org/protocol/ibb' # XEP-0047 -NS_INVISIBLE ='presence-invisible' # Jabberd2 -NS_IQ ='iq' # Jabberd2 -NS_LAST ='jabber:iq:last' # XEP-0012 -NS_MESSAGE ='message' # Jabberd2 -NS_MOOD ='http://jabber.org/protocol/mood' # XEP-0107 -NS_MUC ='http://jabber.org/protocol/muc' # XEP-0045 -NS_MUC_ADMIN =NS_MUC+'#admin' # XEP-0045 -NS_MUC_OWNER =NS_MUC+'#owner' # XEP-0045 -NS_MUC_UNIQUE =NS_MUC+'#unique' # XEP-0045 -NS_MUC_USER =NS_MUC+'#user' # XEP-0045 -NS_MUC_REGISTER =NS_MUC+'#register' # XEP-0045 -NS_MUC_REQUEST =NS_MUC+'#request' # XEP-0045 -NS_MUC_ROOMCONFIG =NS_MUC+'#roomconfig' # XEP-0045 -NS_MUC_ROOMINFO =NS_MUC+'#roominfo' # XEP-0045 -NS_MUC_ROOMS =NS_MUC+'#rooms' # XEP-0045 -NS_MUC_TRAFIC =NS_MUC+'#traffic' # XEP-0045 -NS_NICK ='http://jabber.org/protocol/nick' # XEP-0172 -NS_OFFLINE ='http://jabber.org/protocol/offline' # XEP-0013 -NS_PHYSLOC ='http://jabber.org/protocol/physloc' # XEP-0112 -NS_PRESENCE ='presence' # Jabberd2 -NS_PRIVACY ='jabber:iq:privacy' # RFC 3921 -NS_PRIVATE ='jabber:iq:private' # XEP-0049 -NS_PUBSUB ='http://jabber.org/protocol/pubsub' # XEP-0060 -NS_REGISTER ='jabber:iq:register' # XEP-0077 -NS_RC ='http://jabber.org/protocol/rc' # XEP-0146 -NS_ROSTER ='jabber:iq:roster' # RFC 3921 -NS_ROSTERX ='http://jabber.org/protocol/rosterx' # XEP-0144 -NS_RPC ='jabber:iq:rpc' # XEP-0009 -NS_SASL ='urn:ietf:params:xml:ns:xmpp-sasl' # RFC 3920 -NS_SEARCH ='jabber:iq:search' # XEP-0055 -NS_SERVER ='jabber:server' # RFC 3921 -NS_SESSION ='urn:ietf:params:xml:ns:xmpp-session' # RFC 3921 -NS_SI ='http://jabber.org/protocol/si' # XEP-0096 -NS_SI_PUB ='http://jabber.org/protocol/sipub' # XEP-0137 -NS_SIGNED ='jabber:x:signed' # XEP-0027 -NS_STANZAS ='urn:ietf:params:xml:ns:xmpp-stanzas' # RFC 3920 -NS_STREAMS ='http://etherx.jabber.org/streams' # RFC 3920 -NS_TIME ='jabber:iq:time' # XEP-0090 (deprecated) -NS_TLS ='urn:ietf:params:xml:ns:xmpp-tls' # RFC 3920 -NS_VACATION ='http://jabber.org/protocol/vacation' # XEP-0109 -NS_VCARD ='vcard-temp' # XEP-0054 -NS_VCARD_UPDATE ='vcard-temp:x:update' # XEP-0153 -NS_VERSION ='jabber:iq:version' # XEP-0092 -NS_WAITINGLIST ='http://jabber.org/protocol/waitinglist' # XEP-0130 -NS_XHTML_IM ='http://jabber.org/protocol/xhtml-im' # XEP-0071 -NS_XMPP_STREAMS ='urn:ietf:params:xml:ns:xmpp-streams' # RFC 3920 - -xmpp_stream_error_conditions=""" -bad-format -- -- -- The entity has sent XML that cannot be processed. -bad-namespace-prefix -- -- -- The entity has sent a namespace prefix that is unsupported, or has sent no namespace prefix on an element that requires such a prefix. -conflict -- -- -- The server is closing the active stream for this entity because a new stream has been initiated that conflicts with the existing stream. -connection-timeout -- -- -- The entity has not generated any traffic over the stream for some period of time. -host-gone -- -- -- The value of the 'to' attribute provided by the initiating entity in the stream header corresponds to a hostname that is no longer hosted by the server. -host-unknown -- -- -- The value of the 'to' attribute provided by the initiating entity in the stream header does not correspond to a hostname that is hosted by the server. -improper-addressing -- -- -- A stanza sent between two servers lacks a 'to' or 'from' attribute (or the attribute has no value). -internal-server-error -- -- -- The server has experienced a misconfiguration or an otherwise-undefined internal error that prevents it from servicing the stream. -invalid-from -- cancel -- -- The JID or hostname provided in a 'from' address does not match an authorized JID or validated domain negotiated between servers via SASL or dialback, or between a client and a server via authentication and resource authorization. -invalid-id -- -- -- The stream ID or dialback ID is invalid or does not match an ID previously provided. -invalid-namespace -- -- -- The streams namespace name is something other than "http://etherx.jabber.org/streams" or the dialback namespace name is something other than "jabber:server:dialback". -invalid-xml -- -- -- The entity has sent invalid XML over the stream to a server that performs validation. -not-authorized -- -- -- The entity has attempted to send data before the stream has been authenticated, or otherwise is not authorized to perform an action related to stream negotiation. -policy-violation -- -- -- The entity has violated some local service policy. -remote-connection-failed -- -- -- The server is unable to properly connect to a remote resource that is required for authentication or authorization. -resource-constraint -- -- -- The server lacks the system resources necessary to service the stream. -restricted-xml -- -- -- The entity has attempted to send restricted XML features such as a comment, processing instruction, DTD, entity reference, or unescaped character. -see-other-host -- -- -- The server will not provide service to the initiating entity but is redirecting traffic to another host. -system-shutdown -- -- -- The server is being shut down and all active streams are being closed. -undefined-condition -- -- -- The error condition is not one of those defined by the other conditions in this list. -unsupported-encoding -- -- -- The initiating entity has encoded the stream in an encoding that is not supported by the server. -unsupported-stanza-type -- -- -- The initiating entity has sent a first-level child of the stream that is not supported by the server. -unsupported-version -- -- -- The value of the 'version' attribute provided by the initiating entity in the stream header specifies a version of XMPP that is not supported by the server. -xml-not-well-formed -- -- -- The initiating entity has sent XML that is not well-formed.""" -xmpp_stanza_error_conditions=""" -bad-request -- 400 -- modify -- The sender has sent XML that is malformed or that cannot be processed. -conflict -- 409 -- cancel -- Access cannot be granted because an existing resource or session exists with the same name or address. -feature-not-implemented -- 501 -- cancel -- The feature requested is not implemented by the recipient or server and therefore cannot be processed. -forbidden -- 403 -- auth -- The requesting entity does not possess the required permissions to perform the action. -gone -- 302 -- modify -- The recipient or server can no longer be contacted at this address. -internal-server-error -- 500 -- wait -- The server could not process the stanza because of a misconfiguration or an otherwise-undefined internal server error. -item-not-found -- 404 -- cancel -- The addressed JID or item requested cannot be found. -jid-malformed -- 400 -- modify -- The value of the 'to' attribute in the sender's stanza does not adhere to the syntax defined in Addressing Scheme. -not-acceptable -- 406 -- cancel -- The recipient or server understands the request but is refusing to process it because it does not meet criteria defined by the recipient or server. -not-allowed -- 405 -- cancel -- The recipient or server does not allow any entity to perform the action. -not-authorized -- 401 -- auth -- The sender must provide proper credentials before being allowed to perform the action, or has provided improper credentials. -payment-required -- 402 -- auth -- The requesting entity is not authorized to access the requested service because payment is required. -recipient-unavailable -- 404 -- wait -- The intended recipient is temporarily unavailable. -redirect -- 302 -- modify -- The recipient or server is redirecting requests for this information to another entity. -registration-required -- 407 -- auth -- The requesting entity is not authorized to access the requested service because registration is required. -remote-server-not-found -- 404 -- cancel -- A remote server or service specified as part or all of the JID of the intended recipient does not exist. -remote-server-timeout -- 504 -- wait -- A remote server or service specified as part or all of the JID of the intended recipient could not be contacted within a reasonable amount of time. -resource-constraint -- 500 -- wait -- The server or recipient lacks the system resources necessary to service the request. -service-unavailable -- 503 -- cancel -- The server or recipient does not currently provide the requested service. -subscription-required -- 407 -- auth -- The requesting entity is not authorized to access the requested service because a subscription is required. -undefined-condition -- 500 -- -- -unexpected-request -- 400 -- wait -- The recipient or server understood the request but was not expecting it at this time (e.g., the request was out of order).""" -sasl_error_conditions=""" -aborted -- -- -- The receiving entity acknowledges an element sent by the initiating entity; sent in reply to the element. -incorrect-encoding -- -- -- The data provided by the initiating entity could not be processed because the [BASE64]Josefsson, S., The Base16, Base32, and Base64 Data Encodings, July 2003. encoding is incorrect (e.g., because the encoding does not adhere to the definition in Section 3 of [BASE64]Josefsson, S., The Base16, Base32, and Base64 Data Encodings, July 2003.); sent in reply to a element or an element with initial response data. -invalid-authzid -- -- -- The authzid provided by the initiating entity is invalid, either because it is incorrectly formatted or because the initiating entity does not have permissions to authorize that ID; sent in reply to a element or an element with initial response data. -invalid-mechanism -- -- -- The initiating entity did not provide a mechanism or requested a mechanism that is not supported by the receiving entity; sent in reply to an element. -mechanism-too-weak -- -- -- The mechanism requested by the initiating entity is weaker than server policy permits for that initiating entity; sent in reply to a element or an element with initial response data. -not-authorized -- -- -- The authentication failed because the initiating entity did not provide valid credentials (this includes but is not limited to the case of an unknown username); sent in reply to a element or an element with initial response data. -temporary-auth-failure -- -- -- The authentication failed because of a temporary error condition within the receiving entity; sent in reply to an element or element.""" - -ERRORS,_errorcodes={},{} -for ns,errname,errpool in [(NS_XMPP_STREAMS,'STREAM',xmpp_stream_error_conditions), - (NS_STANZAS ,'ERR' ,xmpp_stanza_error_conditions), - (NS_SASL ,'SASL' ,sasl_error_conditions)]: - for err in errpool.split('\n')[1:]: - cond,code,typ,text=err.split(' -- ') - name=errname+'_'+cond.upper().replace('-','_') - locals()[name]=ns+' '+cond - ERRORS[ns+' '+cond]=[code,typ,text] - if code: _errorcodes[code]=cond -del ns,errname,errpool,err,cond,code,typ,text - -def isResultNode(node): - """ Returns true if the node is a positive reply. """ - return node and node.getType()=='result' -def isErrorNode(node): - """ Returns true if the node is a negative reply. """ - return node and node.getType()=='error' - -class NodeProcessed(Exception): - """ Exception that should be raised by handler when the handling should be stopped. """ -class StreamError(Exception): - """ Base exception class for stream errors.""" -class BadFormat(StreamError): pass -class BadNamespacePrefix(StreamError): pass -class Conflict(StreamError): pass -class ConnectionTimeout(StreamError): pass -class HostGone(StreamError): pass -class HostUnknown(StreamError): pass -class ImproperAddressing(StreamError): pass -class InternalServerError(StreamError): pass -class InvalidFrom(StreamError): pass -class InvalidID(StreamError): pass -class InvalidNamespace(StreamError): pass -class InvalidXML(StreamError): pass -class NotAuthorized(StreamError): pass -class PolicyViolation(StreamError): pass -class RemoteConnectionFailed(StreamError): pass -class ResourceConstraint(StreamError): pass -class RestrictedXML(StreamError): pass -class SeeOtherHost(StreamError): pass -class SystemShutdown(StreamError): pass -class UndefinedCondition(StreamError): pass -class UnsupportedEncoding(StreamError): pass -class UnsupportedStanzaType(StreamError): pass -class UnsupportedVersion(StreamError): pass -class XMLNotWellFormed(StreamError): pass - -stream_exceptions = {'bad-format': BadFormat, - 'bad-namespace-prefix': BadNamespacePrefix, - 'conflict': Conflict, - 'connection-timeout': ConnectionTimeout, - 'host-gone': HostGone, - 'host-unknown': HostUnknown, - 'improper-addressing': ImproperAddressing, - 'internal-server-error': InternalServerError, - 'invalid-from': InvalidFrom, - 'invalid-id': InvalidID, - 'invalid-namespace': InvalidNamespace, - 'invalid-xml': InvalidXML, - 'not-authorized': NotAuthorized, - 'policy-violation': PolicyViolation, - 'remote-connection-failed': RemoteConnectionFailed, - 'resource-constraint': ResourceConstraint, - 'restricted-xml': RestrictedXML, - 'see-other-host': SeeOtherHost, - 'system-shutdown': SystemShutdown, - 'undefined-condition': UndefinedCondition, - 'unsupported-encoding': UnsupportedEncoding, - 'unsupported-stanza-type': UnsupportedStanzaType, - 'unsupported-version': UnsupportedVersion, - 'xml-not-well-formed': XMLNotWellFormed} - -class JID: - """ JID object. JID can be built from string, modified, compared, serialised into string. """ - def __init__(self, jid=None, node='', domain='', resource=''): - """ Constructor. JID can be specified as string (jid argument) or as separate parts. - Examples: - JID('node@domain/resource') - JID(node='node',domain='domain.org') - """ - if not jid and not domain: raise ValueError('JID must contain at least domain name') - elif type(jid)==type(self): self.node,self.domain,self.resource=jid.node,jid.domain,jid.resource - elif domain: self.node,self.domain,self.resource=node,domain,resource - else: - if jid.find('@')+1: self.node,jid=jid.split('@',1) - else: self.node='' - if jid.find('/')+1: self.domain,self.resource=jid.split('/',1) - else: self.domain,self.resource=jid,'' - def getNode(self): - """ Return the node part of the JID """ - return self.node - def setNode(self,node): - """ Set the node part of the JID to new value. Specify None to remove the node part.""" - self.node=node.lower() - def getDomain(self): - """ Return the domain part of the JID """ - return self.domain - def setDomain(self,domain): - """ Set the domain part of the JID to new value.""" - self.domain=domain.lower() - def getResource(self): - """ Return the resource part of the JID """ - return self.resource - def setResource(self,resource): - """ Set the resource part of the JID to new value. Specify None to remove the resource part.""" - self.resource=resource - def getStripped(self): - """ Return the bare representation of JID. I.e. string value w/o resource. """ - return self.__str__(0) - def __eq__(self, other): - """ Compare the JID to another instance or to string for equality. """ - try: other=JID(other) - except ValueError: return 0 - return self.resource==other.resource and self.__str__(0) == other.__str__(0) - def __ne__(self, other): - """ Compare the JID to another instance or to string for non-equality. """ - return not self.__eq__(other) - def bareMatch(self, other): - """ Compare the node and domain parts of the JID's for equality. """ - return self.__str__(0) == JID(other).__str__(0) - def __str__(self,wresource=1): - """ Serialise JID into string. """ - if self.node: jid=self.node+'@'+self.domain - else: jid=self.domain - if wresource and self.resource: return jid+'/'+self.resource - return jid - def __hash__(self): - """ Produce hash of the JID, Allows to use JID objects as keys of the dictionary. """ - return hash(self.__str__()) - -class Protocol(Node): - """ A "stanza" object class. Contains methods that are common for presences, iqs and messages. """ - def __init__(self, name=None, to=None, typ=None, frm=None, attrs={}, payload=[], timestamp=None, xmlns=None, node=None): - """ Constructor, name is the name of the stanza i.e. 'message' or 'presence' or 'iq'. - to is the value of 'to' attribure, 'typ' - 'type' attribute - frn - from attribure, attrs - other attributes mapping, payload - same meaning as for simplexml payload definition - timestamp - the time value that needs to be stamped over stanza - xmlns - namespace of top stanza node - node - parsed or unparsed stana to be taken as prototype. - """ - if not attrs: attrs={} - if to: attrs['to']=to - if frm: attrs['from']=frm - if typ: attrs['type']=typ - Node.__init__(self, tag=name, attrs=attrs, payload=payload, node=node) - if not node and xmlns: self.setNamespace(xmlns) - if self['to']: self.setTo(self['to']) - if self['from']: self.setFrom(self['from']) - if node and type(self)==type(node) and self.__class__==node.__class__ and self.attrs.has_key('id'): del self.attrs['id'] - self.timestamp=None - for x in self.getTags('x',namespace=NS_DELAY): - try: - if not self.getTimestamp() or x.getAttr('stamp')'text': return tag.getName() - return errtag.getData() - def getErrorCode(self): - """ Return the error code. Obsolette. """ - return self.getTagAttr('error','code') - def setError(self,error,code=None): - """ Set the error code. Obsolette. Use error-conditions instead. """ - if code: - if str(code) in _errorcodes.keys(): error=ErrorNode(_errorcodes[str(code)],text=error) - else: error=ErrorNode(ERR_UNDEFINED_CONDITION,code=code,typ='cancel',text=error) - elif type(error) in [type(''),type(u'')]: error=ErrorNode(error) - self.setType('error') - self.addChild(node=error) - def setTimestamp(self,val=None): - """Set the timestamp. timestamp should be the yyyymmddThhmmss string.""" - if not val: val=time.strftime('%Y%m%dT%H:%M:%S', time.gmtime()) - self.timestamp=val - self.setTag('x',{'stamp':self.timestamp},namespace=NS_DELAY) - def getProperties(self): - """ Return the list of namespaces to which belongs the direct childs of element""" - props=[] - for child in self.getChildren(): - prop=child.getNamespace() - if prop not in props: props.append(prop) - return props - def __setitem__(self,item,val): - """ Set the item 'item' to the value 'val'.""" - if item in ['to','from']: val=JID(val) - return self.setAttr(item,val) - -class Message(Protocol): - """ XMPP Message stanza - "push" mechanism.""" - def __init__(self, to=None, body=None, typ=None, subject=None, attrs={}, frm=None, payload=[], timestamp=None, xmlns=NS_CLIENT, node=None): - """ Create message object. You can specify recipient, text of message, type of message - any additional attributes, sender of the message, any additional payload (f.e. jabber:x:delay element) and namespace in one go. - Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as message. """ - Protocol.__init__(self, 'message', to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node) - if body: self.setBody(body) - if subject: self.setSubject(subject) - def getBody(self): - """ Returns text of the message. """ - return self.getTagData('body') - def getSubject(self): - """ Returns subject of the message. """ - return self.getTagData('subject') - def getThread(self): - """ Returns thread of the message. """ - return self.getTagData('thread') - def setBody(self,val): - """ Sets the text of the message. """ - self.setTagData('body',val) - def setSubject(self,val): - """ Sets the subject of the message. """ - self.setTagData('subject',val) - def setThread(self,val): - """ Sets the thread of the message. """ - self.setTagData('thread',val) - def buildReply(self,text=None): - """ Builds and returns another message object with specified text. - The to, from and thread properties of new message are pre-set as reply to this message. """ - m=Message(to=self.getFrom(),frm=self.getTo(),body=text) - th=self.getThread() - if th: m.setThread(th) - return m - -class Presence(Protocol): - """ XMPP Presence object.""" - def __init__(self, to=None, typ=None, priority=None, show=None, status=None, attrs={}, frm=None, timestamp=None, payload=[], xmlns=NS_CLIENT, node=None): - """ Create presence object. You can specify recipient, type of message, priority, show and status values - any additional attributes, sender of the presence, timestamp, any additional payload (f.e. jabber:x:delay element) and namespace in one go. - Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as presence. """ - Protocol.__init__(self, 'presence', to=to, typ=typ, attrs=attrs, frm=frm, payload=payload, timestamp=timestamp, xmlns=xmlns, node=node) - if priority: self.setPriority(priority) - if show: self.setShow(show) - if status: self.setStatus(status) - def getPriority(self): - """ Returns the priority of the message. """ - return self.getTagData('priority') - def getShow(self): - """ Returns the show value of the message. """ - return self.getTagData('show') - def getStatus(self): - """ Returns the status string of the message. """ - return self.getTagData('status') - def setPriority(self,val): - """ Sets the priority of the message. """ - self.setTagData('priority',val) - def setShow(self,val): - """ Sets the show value of the message. """ - self.setTagData('show',val) - def setStatus(self,val): - """ Sets the status string of the message. """ - self.setTagData('status',val) - - def _muc_getItemAttr(self,tag,attr): - for xtag in self.getTags('x'): - for child in xtag.getTags(tag): - return child.getAttr(attr) - def _muc_getSubTagDataAttr(self,tag,attr): - for xtag in self.getTags('x'): - for child in xtag.getTags('item'): - for cchild in child.getTags(tag): - return cchild.getData(),cchild.getAttr(attr) - return None,None - def getRole(self): - """Returns the presence role (for groupchat)""" - return self._muc_getItemAttr('item','role') - def getAffiliation(self): - """Returns the presence affiliation (for groupchat)""" - return self._muc_getItemAttr('item','affiliation') - def getNick(self): - """Returns the nick value (for nick change in groupchat)""" - return self._muc_getItemAttr('item','nick') - def getJid(self): - """Returns the presence jid (for groupchat)""" - return self._muc_getItemAttr('item','jid') - def getReason(self): - """Returns the reason of the presence (for groupchat)""" - return self._muc_getSubTagDataAttr('reason','')[0] - def getActor(self): - """Returns the reason of the presence (for groupchat)""" - return self._muc_getSubTagDataAttr('actor','jid')[1] - def getStatusCode(self): - """Returns the status code of the presence (for groupchat)""" - return self._muc_getItemAttr('status','code') - -class Iq(Protocol): - """ XMPP Iq object - get/set dialog mechanism. """ - def __init__(self, typ=None, queryNS=None, attrs={}, to=None, frm=None, payload=[], xmlns=NS_CLIENT, node=None): - """ Create Iq object. You can specify type, query namespace - any additional attributes, recipient of the iq, sender of the iq, any additional payload (f.e. jabber:x:data node) and namespace in one go. - Alternatively you can pass in the other XML object as the 'node' parameted to replicate it as an iq. """ - Protocol.__init__(self, 'iq', to=to, typ=typ, attrs=attrs, frm=frm, xmlns=xmlns, node=node) - if payload: self.setQueryPayload(payload) - if queryNS: self.setQueryNS(queryNS) - def getQueryNS(self): - """ Return the namespace of the 'query' child element.""" - tag=self.getTag('query') - if tag: return tag.getNamespace() - def getQuerynode(self): - """ Return the 'node' attribute value of the 'query' child element.""" - return self.getTagAttr('query','node') - def getQueryPayload(self): - """ Return the 'query' child element payload.""" - tag=self.getTag('query') - if tag: return tag.getPayload() - def getQueryChildren(self): - """ Return the 'query' child element child nodes.""" - tag=self.getTag('query') - if tag: return tag.getChildren() - def setQueryNS(self,namespace): - """ Set the namespace of the 'query' child element.""" - self.setTag('query').setNamespace(namespace) - def setQueryPayload(self,payload): - """ Set the 'query' child element payload.""" - self.setTag('query').setPayload(payload) - def setQuerynode(self,node): - """ Set the 'node' attribute value of the 'query' child element.""" - self.setTagAttr('query','node',node) - def buildReply(self,typ): - """ Builds and returns another Iq object of specified type. - The to, from and query child node of new Iq are pre-set as reply to this Iq. """ - iq=Iq(typ,to=self.getFrom(),frm=self.getTo(),attrs={'id':self.getID()}) - if self.getTag('query'): iq.setQueryNS(self.getQueryNS()) - return iq - -class ErrorNode(Node): - """ XMPP-style error element. - In the case of stanza error should be attached to XMPP stanza. - In the case of stream-level errors should be used separately. """ - def __init__(self,name,code=None,typ=None,text=None): - """ Create new error node object. - Mandatory parameter: name - name of error condition. - Optional parameters: code, typ, text. Used for backwards compartibility with older jabber protocol.""" - if ERRORS.has_key(name): - cod,type,txt=ERRORS[name] - ns=name.split()[0] - else: cod,ns,type,txt='500',NS_STANZAS,'cancel','' - if typ: type=typ - if code: cod=code - if text: txt=text - Node.__init__(self,'error',{},[Node(name)]) - if type: self.setAttr('type',type) - if not cod: self.setName('stream:error') - if txt: self.addChild(node=Node(ns+' text',{},[txt])) - if cod: self.setAttr('code',cod) - -class Error(Protocol): - """ Used to quickly transform received stanza into error reply.""" - def __init__(self,node,error,reply=1): - """ Create error reply basing on the received 'node' stanza and the 'error' error condition. - If the 'node' is not the received stanza but locally created ('to' and 'from' fields needs not swapping) - specify the 'reply' argument as false.""" - if reply: Protocol.__init__(self,to=node.getFrom(),frm=node.getTo(),node=node) - else: Protocol.__init__(self,node=node) - self.setError(error) - if node.getType()=='error': self.__str__=self.__dupstr__ - def __dupstr__(self,dup1=None,dup2=None): - """ Dummy function used as preventor of creating error node in reply to error node. - I.e. you will not be able to serialise "double" error into string. - """ - return '' - -class DataField(Node): - """ This class is used in the DataForm class to describe the single data item. - If you are working with jabber:x:data (XEP-0004, XEP-0068, XEP-0122) - then you will need to work with instances of this class. """ - def __init__(self,name=None,value=None,typ=None,required=0,label=None,desc=None,options=[],node=None): - """ Create new data field of specified name,value and type. - Also 'required','desc' and 'options' fields can be set. - Alternatively other XML object can be passed in as the 'node' parameted to replicate it as a new datafiled. - """ - Node.__init__(self,'field',node=node) - if name: self.setVar(name) - if type(value) in [list,tuple]: self.setValues(value) - elif value: self.setValue(value) - if typ: self.setType(typ) - elif not typ and not node: self.setType('text-single') - if required: self.setRequired(required) - if label: self.setLabel(label) - if desc: self.setDesc(desc) - if options: self.setOptions(options) - def setRequired(self,req=1): - """ Change the state of the 'required' flag. """ - if req: self.setTag('required') - else: - try: self.delChild('required') - except ValueError: return - def isRequired(self): - """ Returns in this field a required one. """ - return self.getTag('required') - def setLabel(self,label): - """ Set the label of this field. """ - self.setAttr('label',label) - def getLabel(self): - """ Return the label of this field. """ - return self.getAttr('label') - def setDesc(self,desc): - """ Set the description of this field. """ - self.setTagData('desc',desc) - def getDesc(self): - """ Return the description of this field. """ - return self.getTagData('desc') - def setValue(self,val): - """ Set the value of this field. """ - self.setTagData('value',val) - def getValue(self): - return self.getTagData('value') - def setValues(self,lst): - """ Set the values of this field as values-list. - Replaces all previous filed values! If you need to just add a value - use addValue method.""" - while self.getTag('value'): self.delChild('value') - for val in lst: self.addValue(val) - def addValue(self,val): - """ Add one more value to this field. Used in 'get' iq's or such.""" - self.addChild('value',{},[val]) - def getValues(self): - """ Return the list of values associated with this field.""" - ret=[] - for tag in self.getTags('value'): ret.append(tag.getData()) - return ret - def getOptions(self): - """ Return label-option pairs list associated with this field.""" - ret=[] - for tag in self.getTags('option'): ret.append([tag.getAttr('label'),tag.getTagData('value')]) - return ret - def setOptions(self,lst): - """ Set label-option pairs list associated with this field.""" - while self.getTag('option'): self.delChild('option') - for opt in lst: self.addOption(opt) - def addOption(self,opt): - """ Add one more label-option pair to this field.""" - if type(opt) in [str,unicode]: self.addChild('option').setTagData('value',opt) - else: self.addChild('option',{'label':opt[0]}).setTagData('value',opt[1]) - def getType(self): - """ Get type of this field. """ - return self.getAttr('type') - def setType(self,val): - """ Set type of this field. """ - return self.setAttr('type',val) - def getVar(self): - """ Get 'var' attribute value of this field. """ - return self.getAttr('var') - def setVar(self,val): - """ Set 'var' attribute value of this field. """ - return self.setAttr('var',val) - -class DataReported(Node): - """ This class is used in the DataForm class to describe the 'reported data field' data items which are used in - 'multiple item form results' (as described in XEP-0004). - Represents the fields that will be returned from a search. This information is useful when - you try to use the jabber:iq:search namespace to return dynamic form information. - """ - def __init__(self,node=None): - """ Create new empty 'reported data' field. However, note that, according XEP-0004: - * It MUST contain one or more DataFields. - * Contained DataFields SHOULD possess a 'type' and 'label' attribute in addition to 'var' attribute - * Contained DataFields SHOULD NOT contain a element. - Alternatively other XML object can be passed in as the 'node' parameted to replicate it as a new - dataitem. - """ - Node.__init__(self,'reported',node=node) - if node: - newkids=[] - for n in self.getChildren(): - if n.getName()=='field': newkids.append(DataField(node=n)) - else: newkids.append(n) - self.kids=newkids - def getField(self,name): - """ Return the datafield object with name 'name' (if exists). """ - return self.getTag('field',attrs={'var':name}) - def setField(self,name,typ=None,label=None): - """ Create if nessessary or get the existing datafield object with name 'name' and return it. - If created, attributes 'type' and 'label' are applied to new datafield.""" - f=self.getField(name) - if f: return f - return self.addChild(node=DataField(name,None,typ,0,label)) - def asDict(self): - """ Represent dataitem as simple dictionary mapping of datafield names to their values.""" - ret={} - for field in self.getTags('field'): - name=field.getAttr('var') - typ=field.getType() - if isinstance(typ,(str,unicode)) and typ[-6:]=='-multi': - val=[] - for i in field.getTags('value'): val.append(i.getData()) - else: val=field.getTagData('value') - ret[name]=val - if self.getTag('instructions'): ret['instructions']=self.getInstructions() - return ret - def __getitem__(self,name): - """ Simple dictionary interface for getting datafields values by their names.""" - item=self.getField(name) - if item: return item.getValue() - raise IndexError('No such field') - def __setitem__(self,name,val): - """ Simple dictionary interface for setting datafields values by their names.""" - return self.setField(name).setValue(val) - -class DataItem(Node): - """ This class is used in the DataForm class to describe data items which are used in 'multiple - item form results' (as described in XEP-0004). - """ - def __init__(self,node=None): - """ Create new empty data item. However, note that, according XEP-0004, DataItem MUST contain ALL - DataFields described in DataReported. - Alternatively other XML object can be passed in as the 'node' parameted to replicate it as a new - dataitem. - """ - Node.__init__(self,'item',node=node) - if node: - newkids=[] - for n in self.getChildren(): - if n.getName()=='field': newkids.append(DataField(node=n)) - else: newkids.append(n) - self.kids=newkids - def getField(self,name): - """ Return the datafield object with name 'name' (if exists). """ - return self.getTag('field',attrs={'var':name}) - def setField(self,name): - """ Create if nessessary or get the existing datafield object with name 'name' and return it. """ - f=self.getField(name) - if f: return f - return self.addChild(node=DataField(name)) - def asDict(self): - """ Represent dataitem as simple dictionary mapping of datafield names to their values.""" - ret={} - for field in self.getTags('field'): - name=field.getAttr('var') - typ=field.getType() - if isinstance(typ,(str,unicode)) and typ[-6:]=='-multi': - val=[] - for i in field.getTags('value'): val.append(i.getData()) - else: val=field.getTagData('value') - ret[name]=val - if self.getTag('instructions'): ret['instructions']=self.getInstructions() - return ret - def __getitem__(self,name): - """ Simple dictionary interface for getting datafields values by their names.""" - item=self.getField(name) - if item: return item.getValue() - raise IndexError('No such field') - def __setitem__(self,name,val): - """ Simple dictionary interface for setting datafields values by their names.""" - return self.setField(name).setValue(val) - -class DataForm(Node): - """ DataForm class. Used for manipulating dataforms in XMPP. - Relevant XEPs: 0004, 0068, 0122. - Can be used in disco, pub-sub and many other applications.""" - def __init__(self, typ=None, data=[], title=None, node=None): - """ - Create new dataform of type 'typ'; 'data' is the list of DataReported, - DataItem and DataField instances that this dataform contains; 'title' - is the title string. - You can specify the 'node' argument as the other node to be used as - base for constructing this dataform. - - title and instructions is optional and SHOULD NOT contain newlines. - Several instructions MAY be present. - 'typ' can be one of ('form' | 'submit' | 'cancel' | 'result' ) - 'typ' of reply iq can be ( 'result' | 'set' | 'set' | 'result' ) respectively. - 'cancel' form can not contain any fields. All other forms contains AT LEAST one field. - 'title' MAY be included in forms of type "form" and "result" - """ - Node.__init__(self,'x',node=node) - if node: - newkids=[] - for n in self.getChildren(): - if n.getName()=='field': newkids.append(DataField(node=n)) - elif n.getName()=='item': newkids.append(DataItem(node=n)) - elif n.getName()=='reported': newkids.append(DataReported(node=n)) - else: newkids.append(n) - self.kids=newkids - if typ: self.setType(typ) - self.setNamespace(NS_DATA) - if title: self.setTitle(title) - if type(data)==type({}): - newdata=[] - for name in data.keys(): newdata.append(DataField(name,data[name])) - data=newdata - for child in data: - if type(child) in [type(''),type(u'')]: self.addInstructions(child) - elif child.__class__.__name__=='DataField': self.kids.append(child) - elif child.__class__.__name__=='DataItem': self.kids.append(child) - elif child.__class__.__name__=='DataReported': self.kids.append(child) - else: self.kids.append(DataField(node=child)) - def getType(self): - """ Return the type of dataform. """ - return self.getAttr('type') - def setType(self,typ): - """ Set the type of dataform. """ - self.setAttr('type',typ) - def getTitle(self): - """ Return the title of dataform. """ - return self.getTagData('title') - def setTitle(self,text): - """ Set the title of dataform. """ - self.setTagData('title',text) - def getInstructions(self): - """ Return the instructions of dataform. """ - return self.getTagData('instructions') - def setInstructions(self,text): - """ Set the instructions of dataform. """ - self.setTagData('instructions',text) - def addInstructions(self,text): - """ Add one more instruction to the dataform. """ - self.addChild('instructions',{},[text]) - def getField(self,name): - """ Return the datafield object with name 'name' (if exists). """ - return self.getTag('field',attrs={'var':name}) - def setField(self,name): - """ Create if nessessary or get the existing datafield object with name 'name' and return it. """ - f=self.getField(name) - if f: return f - return self.addChild(node=DataField(name)) - def asDict(self): - """ Represent dataform as simple dictionary mapping of datafield names to their values.""" - ret={} - for field in self.getTags('field'): - name=field.getAttr('var') - typ=field.getType() - if isinstance(typ,(str,unicode)) and typ[-6:]=='-multi': - val=[] - for i in field.getTags('value'): val.append(i.getData()) - else: val=field.getTagData('value') - ret[name]=val - if self.getTag('instructions'): ret['instructions']=self.getInstructions() - return ret - def __getitem__(self,name): - """ Simple dictionary interface for getting datafields values by their names.""" - item=self.getField(name) - if item: return item.getValue() - raise IndexError('No such field') - def __setitem__(self,name,val): - """ Simple dictionary interface for setting datafields values by their names.""" - return self.setField(name).setValue(val) diff --git a/sso/services/jabber/xmpp/roster.py b/sso/services/jabber/xmpp/roster.py deleted file mode 100644 index 676a4c9..0000000 --- a/sso/services/jabber/xmpp/roster.py +++ /dev/null @@ -1,184 +0,0 @@ -## roster.py -## -## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - -# $Id: roster.py,v 1.20 2005/07/13 13:22:52 snakeru Exp $ - -""" -Simple roster implementation. Can be used though for different tasks like -mass-renaming of contacts. -""" - -from protocol import * -from client import PlugIn - -class Roster(PlugIn): - """ Defines a plenty of methods that will allow you to manage roster. - Also automatically track presences from remote JIDs taking into - account that every JID can have multiple resources connected. Does not - currently support 'error' presences. - You can also use mapping interface for access to the internal representation of - contacts in roster. - """ - def __init__(self): - """ Init internal variables. """ - PlugIn.__init__(self) - self.DBG_LINE='roster' - self._data = {} - self.set=None - self._exported_methods=[self.getRoster] - - def plugin(self,owner,request=1): - """ Register presence and subscription trackers in the owner's dispatcher. - Also request roster from server if the 'request' argument is set. - Used internally.""" - self._owner.RegisterHandler('iq',self.RosterIqHandler,'result',NS_ROSTER) - self._owner.RegisterHandler('iq',self.RosterIqHandler,'set',NS_ROSTER) - self._owner.RegisterHandler('presence',self.PresenceHandler) - if request: self.Request() - - def Request(self,force=0): - """ Request roster from server if it were not yet requested - (or if the 'force' argument is set). """ - if self.set is None: self.set=0 - elif not force: return - self._owner.send(Iq('get',NS_ROSTER)) - self.DEBUG('Roster requested from server','start') - - def getRoster(self): - """ Requests roster from server if neccessary and returns self.""" - if not self.set: self.Request() - while not self.set: self._owner.Process(10) - return self - - def RosterIqHandler(self,dis,stanza): - """ Subscription tracker. Used internally for setting items state in - internal roster representation. """ - for item in stanza.getTag('query').getTags('item'): - jid=item.getAttr('jid') - if item.getAttr('subscription')=='remove': - if self._data.has_key(jid): del self._data[jid] - raise NodeProcessed # a MUST - self.DEBUG('Setting roster item %s...'%jid,'ok') - if not self._data.has_key(jid): self._data[jid]={} - self._data[jid]['name']=item.getAttr('name') - self._data[jid]['ask']=item.getAttr('ask') - self._data[jid]['subscription']=item.getAttr('subscription') - self._data[jid]['groups']=[] - if not self._data[jid].has_key('resources'): self._data[jid]['resources']={} - for group in item.getTags('group'): self._data[jid]['groups'].append(group.getData()) - self._data[self._owner.User+'@'+self._owner.Server]={'resources':{},'name':None,'ask':None,'subscription':None,'groups':None,} - self.set=1 - raise NodeProcessed # a MUST. Otherwise you'll get back an - - def PresenceHandler(self,dis,pres): - """ Presence tracker. Used internally for setting items' resources state in - internal roster representation. """ - jid=JID(pres.getFrom()) - if not self._data.has_key(jid.getStripped()): self._data[jid.getStripped()]={'name':None,'ask':None,'subscription':'none','groups':['Not in roster'],'resources':{}} - - item=self._data[jid.getStripped()] - typ=pres.getType() - - if not typ: - self.DEBUG('Setting roster item %s for resource %s...'%(jid.getStripped(),jid.getResource()),'ok') - item['resources'][jid.getResource()]=res={'show':None,'status':None,'priority':'0','timestamp':None} - if pres.getTag('show'): res['show']=pres.getShow() - if pres.getTag('status'): res['status']=pres.getStatus() - if pres.getTag('priority'): res['priority']=pres.getPriority() - if not pres.getTimestamp(): pres.setTimestamp() - res['timestamp']=pres.getTimestamp() - elif typ=='unavailable' and item['resources'].has_key(jid.getResource()): del item['resources'][jid.getResource()] - # Need to handle type='error' also - - def _getItemData(self,jid,dataname): - """ Return specific jid's representation in internal format. Used internally. """ - jid=jid[:(jid+'/').find('/')] - return self._data[jid][dataname] - def _getResourceData(self,jid,dataname): - """ Return specific jid's resource representation in internal format. Used internally. """ - if jid.find('/')+1: - jid,resource=jid.split('/',1) - if self._data[jid]['resources'].has_key(resource): return self._data[jid]['resources'][resource][dataname] - elif self._data[jid]['resources'].keys(): - lastpri=-129 - for r in self._data[jid]['resources'].keys(): - if int(self._data[jid]['resources'][r]['priority'])>lastpri: resource,lastpri=r,int(self._data[jid]['resources'][r]['priority']) - return self._data[jid]['resources'][resource][dataname] - def delItem(self,jid): - """ Delete contact 'jid' from roster.""" - self._owner.send(Iq('set',NS_ROSTER,payload=[Node('item',{'jid':jid,'subscription':'remove'})])) - def getAsk(self,jid): - """ Returns 'ask' value of contact 'jid'.""" - return self._getItemData(jid,'ask') - def getGroups(self,jid): - """ Returns groups list that contact 'jid' belongs to.""" - return self._getItemData(jid,'groups') - def getName(self,jid): - """ Returns name of contact 'jid'.""" - return self._getItemData(jid,'name') - def getPriority(self,jid): - """ Returns priority of contact 'jid'. 'jid' should be a full (not bare) JID.""" - return self._getResourceData(jid,'priority') - def getRawRoster(self): - """ Returns roster representation in internal format. """ - return self._data - def getRawItem(self,jid): - """ Returns roster item 'jid' representation in internal format. """ - return self._data[jid[:(jid+'/').find('/')]] - def getShow(self, jid): - """ Returns 'show' value of contact 'jid'. 'jid' should be a full (not bare) JID.""" - return self._getResourceData(jid,'show') - def getStatus(self, jid): - """ Returns 'status' value of contact 'jid'. 'jid' should be a full (not bare) JID.""" - return self._getResourceData(jid,'status') - def getSubscription(self,jid): - """ Returns 'subscription' value of contact 'jid'.""" - return self._getItemData(jid,'subscription') - def getResources(self,jid): - """ Returns list of connected resources of contact 'jid'.""" - return self._data[jid[:(jid+'/').find('/')]]['resources'].keys() - def setItem(self,jid,name=None,groups=[]): - """ Creates/renames contact 'jid' and sets the groups list that it now belongs to.""" - iq=Iq('set',NS_ROSTER) - query=iq.getTag('query') - attrs={'jid':jid} - if name: attrs['name']=name - item=query.setTag('item',attrs) - for group in groups: item.addChild(node=Node('group',payload=[group])) - self._owner.send(iq) - def getItems(self): - """ Return list of all [bare] JIDs that the roster is currently tracks.""" - return self._data.keys() - def keys(self): - """ Same as getItems. Provided for the sake of dictionary interface.""" - return self._data.keys() - def __getitem__(self,item): - """ Get the contact in the internal format. Raises KeyError if JID 'item' is not in roster.""" - return self._data[item] - def getItem(self,item): - """ Get the contact in the internal format (or None if JID 'item' is not in roster).""" - if self._data.has_key(item): return self._data[item] - def Subscribe(self,jid): - """ Send subscription request to JID 'jid'.""" - self._owner.send(Presence(jid,'subscribe')) - def Unsubscribe(self,jid): - """ Ask for removing our subscription for JID 'jid'.""" - self._owner.send(Presence(jid,'unsubscribe')) - def Authorize(self,jid): - """ Authorise JID 'jid'. Works only if these JID requested auth previously. """ - self._owner.send(Presence(jid,'subscribed')) - def Unauthorize(self,jid): - """ Unauthorise JID 'jid'. Use for declining authorisation request - or for removing existing authorization. """ - self._owner.send(Presence(jid,'unsubscribed')) diff --git a/sso/services/jabber/xmpp/session.py b/sso/services/jabber/xmpp/session.py deleted file mode 100644 index 24066b3..0000000 --- a/sso/services/jabber/xmpp/session.py +++ /dev/null @@ -1,349 +0,0 @@ -## -## XMPP server -## -## Copyright (C) 2004 Alexey "Snake" Nezhdanov -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - -__version__="$Id" - -""" -When your handler is called it is getting the session instance as the first argument. -This is the difference from xmpppy 0.1 where you got the "Client" instance. -With Session class you can have "multi-session" client instead of having -one client for each connection. Is is specifically important when you are -writing the server. -""" - -from protocol import * - -# Transport-level flags -SOCKET_UNCONNECTED =0 -SOCKET_ALIVE =1 -SOCKET_DEAD =2 -# XML-level flags -STREAM__NOT_OPENED =1 -STREAM__OPENED =2 -STREAM__CLOSING =3 -STREAM__CLOSED =4 -# XMPP-session flags -SESSION_NOT_AUTHED =1 -SESSION_AUTHED =2 -SESSION_BOUND =3 -SESSION_OPENED =4 -SESSION_CLOSED =5 - -class Session: - """ - The Session class instance is used for storing all session-related info like - credentials, socket/xml stream/session state flags, roster items (in case of - client type connection) etc. - Session object have no means of discovering is any info is ready to be read. - Instead you should use poll() (recomended) or select() methods for this purpose. - Session can be one of two types: 'server' and 'client'. 'server' session handles - inbound connection and 'client' one used to create an outbound one. - Session instance have multitude of internal attributes. The most imporant is the 'peer' one. - It is set once the peer is authenticated (client). - """ - def __init__(self,socket,owner,xmlns=None,peer=None): - """ When the session is created it's type (client/server) is determined from the beginning. - socket argument is the pre-created socket-like object. - It must have the following methods: send, recv, fileno, close. - owner is the 'master' instance that have Dispatcher plugged into it and generally - will take care about all session events. - xmlns is the stream namespace that will be used. Client must set this argument - If server sets this argument than stream will be dropped if opened with some another namespace. - peer is the name of peer instance. This is the flag that differentiates client session from - server session. Client must set it to the name of the server that will be connected, server must - leave this argument alone. - """ - self.xmlns=xmlns - if peer: - self.TYP='client' - self.peer=peer - self._socket_state=SOCKET_UNCONNECTED - else: - self.TYP='server' - self.peer=None - self._socket_state=SOCKET_ALIVE - self._sock=socket - self._send=socket.send - self._recv=socket.recv - self.fileno=socket.fileno - self._registered=0 - - self.Dispatcher=owner.Dispatcher - self.DBG_LINE='session' - self.DEBUG=owner.Dispatcher.DEBUG - self._expected={} - self._owner=owner - if self.TYP=='server': self.ID=`random.random()`[2:] - else: self.ID=None - - self.sendbuffer='' - self._stream_pos_queued=None - self._stream_pos_sent=0 - self.deliver_key_queue=[] - self.deliver_queue_map={} - self.stanza_queue=[] - - self._session_state=SESSION_NOT_AUTHED - self.waiting_features=[] - for feature in [NS_TLS,NS_SASL,NS_BIND,NS_SESSION]: - if feature in owner.features: self.waiting_features.append(feature) - self.features=[] - self.feature_in_process=None - self.slave_session=None - self.StartStream() - - def StartStream(self): - """ This method is used to initialise the internal xml expat parser - and to send initial stream header (in case of client connection). - Should be used after initial connection and after every stream restart.""" - self._stream_state=STREAM__NOT_OPENED - self.Stream=simplexml.NodeBuilder() - self.Stream._dispatch_depth=2 - self.Stream.dispatch=self._dispatch - self.Parse=self.Stream.Parse - self.Stream.stream_footer_received=self._stream_close - if self.TYP=='client': - self.Stream.stream_header_received=self._catch_stream_id - self._stream_open() - else: - self.Stream.stream_header_received=self._stream_open - - def receive(self): - """ Reads all pending incoming data. - Raises IOError on disconnection. - Blocks until at least one byte is read.""" - try: received = self._recv(10240) - except: received = '' - - if len(received): # length of 0 means disconnect - self.DEBUG(`self.fileno()`+' '+received,'got') - else: - self.DEBUG('Socket error while receiving data','error') - self.set_socket_state(SOCKET_DEAD) - raise IOError("Peer disconnected") - return received - - def sendnow(self,chunk): - """ Put chunk into "immidiatedly send" queue. - Should only be used for auth/TLS stuff and like. - If you just want to shedule regular stanza for delivery use enqueue method. - """ - if isinstance(chunk,Node): chunk = chunk.__str__().encode('utf-8') - elif type(chunk)==type(u''): chunk = chunk.encode('utf-8') - self.enqueue(chunk) - - def enqueue(self,stanza): - """ Takes Protocol instance as argument. - Puts stanza into "send" fifo queue. Items into the send queue are hold until - stream authenticated. After that this method is effectively the same as "sendnow" method.""" - if isinstance(stanza,Protocol): - self.stanza_queue.append(stanza) - else: self.sendbuffer+=stanza - if self._socket_state>=SOCKET_ALIVE: self.push_queue() - - def push_queue(self,failreason=ERR_RECIPIENT_UNAVAILABLE): - """ If stream is authenticated than move items from "send" queue to "immidiatedly send" queue. - Else if the stream is failed then return all queued stanzas with error passed as argument. - Otherwise do nothing.""" - # If the stream authed - convert stanza_queue into sendbuffer and set the checkpoints - - if self._stream_state>=STREAM__CLOSED or self._socket_state>=SOCKET_DEAD: # the stream failed. Return all stanzas that are still waiting for delivery. - self._owner.deactivatesession(self) - for key in self.deliver_key_queue: # Not sure. May be I - self._dispatch(Error(self.deliver_queue_map[key],failreason),trusted=1) # should simply re-dispatch it? - for stanza in self.stanza_queue: # But such action can invoke - self._dispatch(Error(stanza,failreason),trusted=1) # Infinite loops in case of S2S connection... - self.deliver_queue_map,self.deliver_key_queue,self.stanza_queue={},[],[] - return - elif self._session_state>=SESSION_AUTHED: # FIXME! Должен быть какой-то другой флаг. - #### LOCK_QUEUE - for stanza in self.stanza_queue: - txt=stanza.__str__().encode('utf-8') - self.sendbuffer+=txt - self._stream_pos_queued+=len(txt) # should be re-evaluated for SSL connection. - self.deliver_queue_map[self._stream_pos_queued]=stanza # position of the stream when stanza will be successfully and fully sent - self.deliver_key_queue.append(self._stream_pos_queued) - self.stanza_queue=[] - #### UNLOCK_QUEUE - - def flush_queue(self): - """ Put the "immidiatedly send" queue content on the wire. Blocks until at least one byte sent.""" - if self.sendbuffer: - try: - # LOCK_QUEUE - sent=self._send(self.sendbuffer) # Блокирующая штучка! - except: - # UNLOCK_QUEUE - self.set_socket_state(SOCKET_DEAD) - self.DEBUG("Socket error while sending data",'error') - return self.terminate_stream() - self.DEBUG(`self.fileno()`+' '+self.sendbuffer[:sent],'sent') - self._stream_pos_sent+=sent - self.sendbuffer=self.sendbuffer[sent:] - self._stream_pos_delivered=self._stream_pos_sent # Should be acquired from socket somehow. Take SSL into account. - while self.deliver_key_queue and self._stream_pos_delivered>self.deliver_key_queue[0]: - del self.deliver_queue_map[self.deliver_key_queue[0]] - self.deliver_key_queue.remove(self.deliver_key_queue[0]) - # UNLOCK_QUEUE - - def _dispatch(self,stanza,trusted=0): - """ This is callback that is used to pass the received stanza forth to owner's dispatcher - _if_ the stream is authorised. Otherwise the stanza is just dropped. - The 'trusted' argument is used to emulate stanza receive. - This method is used internally. - """ - self._owner.packets+=1 - if self._stream_state==STREAM__OPENED or trusted: # if the server really should reject all stanzas after he is closed stream (himeself)? - self.DEBUG(stanza.__str__(),'dispatch') - stanza.trusted=trusted - return self.Dispatcher.dispatch(stanza,self) - - def _catch_stream_id(self,ns=None,tag='stream',attrs={}): - """ This callback is used to detect the stream namespace of incoming stream. Used internally. """ - if not attrs.has_key('id') or not attrs['id']: - return self.terminate_stream(STREAM_INVALID_XML) - self.ID=attrs['id'] - if not attrs.has_key('version'): self._owner.Dialback(self) - - def _stream_open(self,ns=None,tag='stream',attrs={}): - """ This callback is used to handle opening stream tag of the incoming stream. - In the case of client session it just make some validation. - Server session also sends server headers and if the stream valid the features node. - Used internally. """ - text='\n') - self.set_stream_state(STREAM__OPENED) - if self.TYP=='client': return - if tag<>'stream': return self.terminate_stream(STREAM_INVALID_XML) - if ns<>NS_STREAMS: return self.terminate_stream(STREAM_INVALID_NAMESPACE) - if self.Stream.xmlns<>self.xmlns: return self.terminate_stream(STREAM_BAD_NAMESPACE_PREFIX) - if not attrs.has_key('to'): return self.terminate_stream(STREAM_IMPROPER_ADDRESSING) - if attrs['to'] not in self._owner.servernames: return self.terminate_stream(STREAM_HOST_UNKNOWN) - self.ourname=attrs['to'].lower() - if self.TYP=='server' and attrs.has_key('version'): - # send features - features=Node('stream:features') - if NS_TLS in self.waiting_features: - features.NT.starttls.setNamespace(NS_TLS) - features.T.starttls.NT.required - if NS_SASL in self.waiting_features: - features.NT.mechanisms.setNamespace(NS_SASL) - for mec in self._owner.SASL.mechanisms: - features.T.mechanisms.NT.mechanism=mec - else: - if NS_BIND in self.waiting_features: features.NT.bind.setNamespace(NS_BIND) - if NS_SESSION in self.waiting_features: features.NT.session.setNamespace(NS_SESSION) - self.sendnow(features) - - def feature(self,feature): - """ Declare some stream feature as activated one. """ - if feature not in self.features: self.features.append(feature) - self.unfeature(feature) - - def unfeature(self,feature): - """ Declare some feature as illegal. Illegal features can not be used. - Example: BIND feature becomes illegal after Non-SASL auth. """ - if feature in self.waiting_features: self.waiting_features.remove(feature) - - def _stream_close(self,unregister=1): - """ Write the closing stream tag and destroy the underlaying socket. Used internally. """ - if self._stream_state>=STREAM__CLOSED: return - self.set_stream_state(STREAM__CLOSING) - self.sendnow('') - self.set_stream_state(STREAM__CLOSED) - self.push_queue() # decompose queue really since STREAM__CLOSED - self._owner.flush_queues() - if unregister: self._owner.unregistersession(self) - self._destroy_socket() - - def terminate_stream(self,error=None,unregister=1): - """ Notify the peer about stream closure. - Ensure that xmlstream is not brokes - i.e. if the stream isn't opened yet - - open it before closure. - If the error condition is specified than create a stream error and send it along with - closing stream tag. - Emulate receiving 'unavailable' type presence just before stream closure. - """ - if self._stream_state>=STREAM__CLOSING: return - if self._stream_statef: raise "Stopping feature %s instead of %s !"%(f,self.feature_in_process) - self.feature_in_process=None - - def set_socket_state(self,newstate): - """ Change the underlaying socket state. - Socket starts with SOCKET_UNCONNECTED state - and then proceeds (possibly) to SOCKET_ALIVE - and then to SOCKET_DEAD """ - if self._socket_state=SESSION_AUTHED: self._stream_pos_queued=self._stream_pos_sent - self._session_state=newstate - - def set_stream_state(self,newstate): - """ Change the underlaying XML stream state - Stream starts with STREAM__NOT_OPENED and then proceeds with - STREAM__OPENED, STREAM__CLOSING and STREAM__CLOSED states. - Note that some features (like TLS and SASL) - requires stream re-start so this state can have non-linear changes. """ - if self._stream_state " replaced by their respective XML entities.""" - # replace also FORM FEED and ESC, because they are not valid XML chars - return txt.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """).replace(u'\x0C', "").replace(u'\x1B', "") - -ENCODING='utf-8' -def ustr(what): - """Converts object "what" to unicode string using it's own __str__ method if accessible or unicode method otherwise.""" - if isinstance(what, unicode): return what - try: r=what.__str__() - except AttributeError: r=str(what) - if not isinstance(r, unicode): return unicode(r,ENCODING) - return r - -class Node(object): - """ Node class describes syntax of separate XML Node. It have a constructor that permits node creation - from set of "namespace name", attributes and payload of text strings and other nodes. - It does not natively support building node from text string and uses NodeBuilder class for that purpose. - After creation node can be mangled in many ways so it can be completely changed. - Also node can be serialised into string in one of two modes: default (where the textual representation - of node describes it exactly) and "fancy" - with whitespace added to make indentation and thus make - result more readable by human. - - Node class have attribute FORCE_NODE_RECREATION that is defaults to False thus enabling fast node - replication from the some other node. The drawback of the fast way is that new node shares some - info with the "original" node that is changing the one node may influence the other. Though it is - rarely needed (in xmpppy it is never needed at all since I'm usually never using original node after - replication (and using replication only to move upwards on the classes tree). - """ - FORCE_NODE_RECREATION=0 - def __init__(self, tag=None, attrs={}, payload=[], parent=None, nsp=None, node_built=False, node=None): - """ Takes "tag" argument as the name of node (prepended by namespace, if needed and separated from it - by a space), attrs dictionary as the set of arguments, payload list as the set of textual strings - and child nodes that this node carries within itself and "parent" argument that is another node - that this one will be the child of. Also the __init__ can be provided with "node" argument that is - either a text string containing exactly one node or another Node instance to begin with. If both - "node" and other arguments is provided then the node initially created as replica of "node" - provided and then modified to be compliant with other arguments.""" - if node: - if self.FORCE_NODE_RECREATION and isinstance(node, Node): - node=str(node) - if not isinstance(node, Node): - node=NodeBuilder(node,self) - node_built = True - else: - self.name,self.namespace,self.attrs,self.data,self.kids,self.parent,self.nsd = node.name,node.namespace,{},[],[],node.parent,{} - for key in node.attrs.keys(): self.attrs[key]=node.attrs[key] - for data in node.data: self.data.append(data) - for kid in node.kids: self.kids.append(kid) - for k,v in node.nsd.items(): self.nsd[k] = v - else: self.name,self.namespace,self.attrs,self.data,self.kids,self.parent,self.nsd = 'tag','',{},[],[],None,{} - if parent: - self.parent = parent - self.nsp_cache = {} - if nsp: - for k,v in nsp.items(): self.nsp_cache[k] = v - for attr,val in attrs.items(): - if attr == 'xmlns': - self.nsd[u''] = val - elif attr.startswith('xmlns:'): - self.nsd[attr[6:]] = val - self.attrs[attr]=attrs[attr] - if tag: - if node_built: - pfx,self.name = (['']+tag.split(':'))[-2:] - self.namespace = self.lookup_nsp(pfx) - else: - if ' ' in tag: - self.namespace,self.name = tag.split() - else: - self.name = tag - if isinstance(payload, basestring): payload=[payload] - for i in payload: - if isinstance(i, Node): self.addChild(node=i) - else: self.data.append(ustr(i)) - - def lookup_nsp(self,pfx=''): - ns = self.nsd.get(pfx,None) - if ns is None: - ns = self.nsp_cache.get(pfx,None) - if ns is None: - if self.parent: - ns = self.parent.lookup_nsp(pfx) - self.nsp_cache[pfx] = ns - else: - return 'http://www.gajim.org/xmlns/undeclared' - return ns - - def __str__(self,fancy=0): - """ Method used to dump node into textual representation. - if "fancy" argument is set to True produces indented output for readability.""" - s = (fancy-1) * 2 * ' ' + "<" + self.name - if self.namespace: - if not self.parent or self.parent.namespace!=self.namespace: - if 'xmlns' not in self.attrs: - s = s + ' xmlns="%s"'%self.namespace - for key in self.attrs.keys(): - val = ustr(self.attrs[key]) - s = s + ' %s="%s"' % ( key, XMLescape(val) ) - s = s + ">" - cnt = 0 - if self.kids: - if fancy: s = s + "\n" - for a in self.kids: - if not fancy and (len(self.data)-1)>=cnt: s=s+XMLescape(self.data[cnt]) - elif (len(self.data)-1)>=cnt: s=s+XMLescape(self.data[cnt].strip()) - if isinstance(a, Node): - s = s + a.__str__(fancy and fancy+1) - elif a: - s = s + a.__str__() - cnt=cnt+1 - if not fancy and (len(self.data)-1) >= cnt: s = s + XMLescape(self.data[cnt]) - elif (len(self.data)-1) >= cnt: s = s + XMLescape(self.data[cnt].strip()) - if not self.kids and s.endswith('>'): - s=s[:-1]+' />' - if fancy: s = s + "\n" - else: - if fancy and not self.data: s = s + (fancy-1) * 2 * ' ' - s = s + "" - if fancy: s = s + "\n" - return s - def getCDATA(self): - """ Serialise node, dropping all tags and leaving CDATA intact. - That is effectively kills all formatiing, leaving only text were contained in XML. - """ - s = "" - cnt = 0 - if self.kids: - for a in self.kids: - s=s+self.data[cnt] - if a: s = s + a.getCDATA() - cnt=cnt+1 - if (len(self.data)-1) >= cnt: s = s + self.data[cnt] - return s - def addChild(self, name=None, attrs={}, payload=[], namespace=None, node=None): - """ If "node" argument is provided, adds it as child node. Else creates new node from - the other arguments' values and adds it as well.""" - if 'xmlns' in attrs: - raise AttributeError("Use namespace=x instead of attrs={'xmlns':x}") - if node: - newnode=node - node.parent = self - else: newnode=Node(tag=name, parent=self, attrs=attrs, payload=payload) - if namespace: - newnode.setNamespace(namespace) - self.kids.append(newnode) - self.data.append(u'') - return newnode - def addData(self, data): - """ Adds some CDATA to node. """ - self.data.append(ustr(data)) - self.kids.append(None) - def clearData(self): - """ Removes all CDATA from the node. """ - self.data=[] - def delAttr(self, key): - """ Deletes an attribute "key" """ - del self.attrs[key] - def delChild(self, node, attrs={}): - """ Deletes the "node" from the node's childs list, if "node" is an instance. - Else deletes the first node that have specified name and (optionally) attributes. """ - if not isinstance(node, Node): node=self.getTag(node,attrs) - self.kids[self.kids.index(node)]=None - return node - def getAttrs(self): - """ Returns all node's attributes as dictionary. """ - return self.attrs - def getAttr(self, key): - """ Returns value of specified attribute. """ - try: return self.attrs[key] - except: return None - def getChildren(self): - """ Returns all node's child nodes as list. """ - return self.kids - def getData(self): - """ Returns all node CDATA as string (concatenated). """ - return ''.join(self.data) - def getName(self): - """ Returns the name of node """ - return self.name - def getNamespace(self): - """ Returns the namespace of node """ - return self.namespace - def getParent(self): - """ Returns the parent of node (if present). """ - return self.parent - def getPayload(self): - """ Return the payload of node i.e. list of child nodes and CDATA entries. - F.e. for "text1 text2" will be returned list: - ['text1', , , ' text2']. """ - ret=[] - for i in range(max(len(self.data),len(self.kids))): - if i < len(self.data) and self.data[i]: ret.append(self.data[i]) - if i < len(self.kids) and self.kids[i]: ret.append(self.kids[i]) - return ret - def getTag(self, name, attrs={}, namespace=None): - """ Filters all child nodes using specified arguments as filter. - Returns the first found or None if not found. """ - return self.getTags(name, attrs, namespace, one=1) - def getTagAttr(self,tag,attr): - """ Returns attribute value of the child with specified name (or None if no such attribute).""" - try: return self.getTag(tag).attrs[attr] - except: return None - def getTagData(self,tag): - """ Returns cocatenated CDATA of the child with specified name.""" - try: return self.getTag(tag).getData() - except: return None - def getTags(self, name, attrs={}, namespace=None, one=0): - """ Filters all child nodes using specified arguments as filter. - Returns the list of nodes found. """ - nodes=[] - for node in self.kids: - if not node: continue - if namespace and namespace!=node.getNamespace(): continue - if node.getName() == name: - for key in attrs.keys(): - if key not in node.attrs or node.attrs[key]!=attrs[key]: break - else: nodes.append(node) - if one and nodes: return nodes[0] - if not one: return nodes - - def iterTags(self, name, attrs={}, namespace=None): - """ Iterate over all children using specified arguments as filter. """ - for node in self.kids: - if not node: continue - if namespace is not None and namespace!=node.getNamespace(): continue - if node.getName() == name: - for key in attrs.keys(): - if key not in node.attrs or \ - node.attrs[key]!=attrs[key]: break - else: - yield node - - def setAttr(self, key, val): - """ Sets attribute "key" with the value "val". """ - self.attrs[key]=val - def setData(self, data): - """ Sets node's CDATA to provided string. Resets all previous CDATA!""" - self.data=[ustr(data)] - def setName(self,val): - """ Changes the node name. """ - self.name = val - def setNamespace(self, namespace): - """ Changes the node namespace. """ - self.namespace=namespace - def setParent(self, node): - """ Sets node's parent to "node". WARNING: do not checks if the parent already present - and not removes the node from the list of childs of previous parent. """ - self.parent = node - def setPayload(self,payload,add=0): - """ Sets node payload according to the list specified. WARNING: completely replaces all node's - previous content. If you wish just to add child or CDATA - use addData or addChild methods. """ - if isinstance(payload, basestring): payload=[payload] - if add: self.kids+=payload - else: self.kids=payload - def setTag(self, name, attrs={}, namespace=None): - """ Same as getTag but if the node with specified namespace/attributes not found, creates such - node and returns it. """ - node=self.getTags(name, attrs, namespace=namespace, one=1) - if node: return node - else: return self.addChild(name, attrs, namespace=namespace) - def setTagAttr(self,tag,attr,val): - """ Creates new node (if not already present) with name "tag" - and sets it's attribute "attr" to value "val". """ - try: self.getTag(tag).attrs[attr]=val - except: self.addChild(tag,attrs={attr:val}) - def setTagData(self,tag,val,attrs={}): - """ Creates new node (if not already present) with name "tag" and (optionally) attributes "attrs" - and sets it's CDATA to string "val". """ - try: self.getTag(tag,attrs).setData(ustr(val)) - except: self.addChild(tag,attrs,payload=[ustr(val)]) - def has_attr(self,key): - """ Checks if node have attribute "key".""" - return key in self.attrs - def __getitem__(self,item): - """ Returns node's attribute "item" value. """ - return self.getAttr(item) - def __setitem__(self,item,val): - """ Sets node's attribute "item" value. """ - return self.setAttr(item,val) - def __delitem__(self,item): - """ Deletes node's attribute "item". """ - return self.delAttr(item) - def __getattr__(self,attr): - """ Reduce memory usage caused by T/NT classes - use memory only when needed. """ - if attr=='T': - self.T=T(self) - return self.T - if attr=='NT': - self.NT=NT(self) - return self.NT - raise AttributeError - -class T: - """ Auxiliary class used to quick access to node's child nodes. """ - def __init__(self,node): self.__dict__['node']=node - def __getattr__(self,attr): return self.node.getTag(attr) - def __setattr__(self,attr,val): - if isinstance(val,Node): Node.__init__(self.node.setTag(attr),node=val) - else: return self.node.setTagData(attr,val) - def __delattr__(self,attr): return self.node.delChild(attr) - -class NT(T): - """ Auxiliary class used to quick create node's child nodes. """ - def __getattr__(self,attr): return self.node.addChild(attr) - def __setattr__(self,attr,val): - if isinstance(val,Node): self.node.addChild(attr,node=val) - else: return self.node.addChild(attr,payload=[val]) - -DBG_NODEBUILDER = 'nodebuilder' -class NodeBuilder: - """ Builds a Node class minidom from data parsed to it. This class used for two purposes: - 1. Creation an XML Node from a textual representation. F.e. reading a config file. See an XML2Node method. - 2. Handling an incoming XML stream. This is done by mangling - the __dispatch_depth parameter and redefining the dispatch method. - You do not need to use this class directly if you do not designing your own XML handler.""" - def __init__(self,data=None,initial_node=None): - """ Takes two optional parameters: "data" and "initial_node". - By default class initialised with empty Node class instance. - Though, if "initial_node" is provided it used as "starting point". - You can think about it as of "node upgrade". - "data" (if provided) feeded to parser immidiatedly after instance init. - """ - self.DEBUG(DBG_NODEBUILDER, "Preparing to handle incoming XML stream.", 'start') - self._parser = xml.parsers.expat.ParserCreate() - self._parser.StartElementHandler = self.starttag - self._parser.EndElementHandler = self.endtag - self._parser.CharacterDataHandler = self.handle_cdata - self._parser.StartNamespaceDeclHandler = self.handle_namespace_start - self._parser.buffer_text = True - self.Parse = self._parser.Parse - - self.__depth = 0 - self.__last_depth = 0 - self.__max_depth = 0 - self._dispatch_depth = 1 - self._document_attrs = None - self._document_nsp = None - self._mini_dom=initial_node - self.last_is_data = 1 - self._ptr=None - self.data_buffer = None - self.streamError = '' - if data: - self._parser.Parse(data,1) - - def check_data_buffer(self): - if self.data_buffer: - self._ptr.data.append(''.join(self.data_buffer)) - del self.data_buffer[:] - self.data_buffer = None - - def destroy(self): - """ Method used to allow class instance to be garbage-collected. """ - self.check_data_buffer() - self._parser.StartElementHandler = None - self._parser.EndElementHandler = None - self._parser.CharacterDataHandler = None - self._parser.StartNamespaceDeclHandler = None - - def starttag(self, tag, attrs): - """XML Parser callback. Used internally""" - self.check_data_buffer() - self._inc_depth() - self.DEBUG(DBG_NODEBUILDER, "DEPTH -> %i , tag -> %s, attrs -> %s" % (self.__depth, tag, `attrs`), 'down') - if self.__depth == self._dispatch_depth: - if not self._mini_dom : - self._mini_dom = Node(tag=tag, attrs=attrs, nsp = self._document_nsp, node_built=True) - else: - Node.__init__(self._mini_dom,tag=tag, attrs=attrs, nsp = self._document_nsp, node_built=True) - self._ptr = self._mini_dom - elif self.__depth > self._dispatch_depth: - self._ptr.kids.append(Node(tag=tag,parent=self._ptr,attrs=attrs, node_built=True)) - self._ptr = self._ptr.kids[-1] - if self.__depth == 1: - self._document_attrs = {} - self._document_nsp = {} - nsp, name = (['']+tag.split(':'))[-2:] - for attr,val in attrs.items(): - if attr == 'xmlns': - self._document_nsp[u''] = val - elif attr.startswith('xmlns:'): - self._document_nsp[attr[6:]] = val - else: - self._document_attrs[attr] = val - ns = self._document_nsp.get(nsp, 'http://www.gajim.org/xmlns/undeclared-root') - try: - self.stream_header_received(ns, name, attrs) - except ValueError, e: - self._document_attrs = None - raise ValueError(str(e)) - if not self.last_is_data and self._ptr.parent: - self._ptr.parent.data.append('') - self.last_is_data = 0 - - def endtag(self, tag ): - """XML Parser callback. Used internally""" - self.DEBUG(DBG_NODEBUILDER, "DEPTH -> %i , tag -> %s" % (self.__depth, tag), 'up') - self.check_data_buffer() - if self.__depth == self._dispatch_depth: - if self._mini_dom.getName() == 'error': - self.streamError = self._mini_dom.getChildren()[0].getName() - self.dispatch(self._mini_dom) - elif self.__depth > self._dispatch_depth: - self._ptr = self._ptr.parent - else: - self.DEBUG(DBG_NODEBUILDER, "Got higher than dispatch level. Stream terminated?", 'stop') - self._dec_depth() - self.last_is_data = 0 - if self.__depth == 0: self.stream_footer_received() - - def handle_cdata(self, data): - """XML Parser callback. Used internally""" - self.DEBUG(DBG_NODEBUILDER, data, 'data') - if self.last_is_data: - if self.data_buffer: - self.data_buffer.append(data) - elif self._ptr: - self.data_buffer = [data] - self.last_is_data = 1 - - def handle_namespace_start(self, prefix, uri): - """XML Parser callback. Used internally""" - self.check_data_buffer() - - def DEBUG(self, level, text, comment=None): - """ Gets all NodeBuilder walking events. Can be used for debugging if redefined.""" - def getDom(self): - """ Returns just built Node. """ - self.check_data_buffer() - return self._mini_dom - def dispatch(self,stanza): - """ Gets called when the NodeBuilder reaches some level of depth on it's way up with the built - node as argument. Can be redefined to convert incoming XML stanzas to program events. """ - def stream_header_received(self,ns,tag,attrs): - """ Method called when stream just opened. """ - self.check_data_buffer() - def stream_footer_received(self): - """ Method called when stream just closed. """ - self.check_data_buffer() - - def has_received_endtag(self, level=0): - """ Return True if at least one end tag was seen (at level) """ - return self.__depth <= level and self.__max_depth > level - - def _inc_depth(self): - self.__last_depth = self.__depth - self.__depth += 1 - self.__max_depth = max(self.__depth, self.__max_depth) - - def _dec_depth(self): - self.__last_depth = self.__depth - self.__depth -= 1 - -def XML2Node(xml): - """ Converts supplied textual string into XML node. Handy f.e. for reading configuration file. - Raises xml.parser.expat.parsererror if provided string is not well-formed XML. """ - return NodeBuilder(xml).getDom() - -def BadXML2Node(xml): - """ Converts supplied textual string into XML node. Survives if xml data is cutted half way round. - I.e. "some text
    some more text". Will raise xml.parser.expat.parsererror on misplaced - tags though. F.e. "some text
    some more text
    " will not work.""" - return NodeBuilder(xml).getDom() diff --git a/sso/services/jabber/xmpp/transports.py b/sso/services/jabber/xmpp/transports.py deleted file mode 100644 index 0e3eec9..0000000 --- a/sso/services/jabber/xmpp/transports.py +++ /dev/null @@ -1,339 +0,0 @@ -## transports.py -## -## Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - -# $Id: transports.py,v 1.35 2009/04/07 08:34:09 snakeru Exp $ - -""" -This module contains the low-level implementations of xmpppy connect methods or -(in other words) transports for xmpp-stanzas. -Currently here is three transports: -direct TCP connect - TCPsocket class -proxied TCP connect - HTTPPROXYsocket class (CONNECT proxies) -TLS connection - TLS class. Can be used for SSL connections also. - -Transports are stackable so you - f.e. TLS use HTPPROXYsocket or TCPsocket as more low-level transport. - -Also exception 'error' is defined to allow capture of this module specific exceptions. -""" - -import socket,select,base64,dispatcher,sys -from simplexml import ustr -from client import PlugIn -from protocol import * - -# determine which DNS resolution library is available -HAVE_DNSPYTHON = False -HAVE_PYDNS = False -try: - import dns.resolver # http://dnspython.org/ - HAVE_DNSPYTHON = True -except ImportError: - try: - import DNS # http://pydns.sf.net/ - HAVE_PYDNS = True - except ImportError: - pass - -DATA_RECEIVED='DATA RECEIVED' -DATA_SENT='DATA SENT' - -class error: - """An exception to be raised in case of low-level errors in methods of 'transports' module.""" - def __init__(self,comment): - """Cache the descriptive string""" - self._comment=comment - - def __str__(self): - """Serialise exception into pre-cached descriptive string.""" - return self._comment - -BUFLEN=1024 -class TCPsocket(PlugIn): - """ This class defines direct TCP connection method. """ - def __init__(self, server=None, use_srv=True): - """ Cache connection point 'server'. 'server' is the tuple of (host, port) - absolutely the same as standard tcp socket uses. However library will lookup for - ('_xmpp-client._tcp.' + host) SRV record in DNS and connect to the found (if it is) - server instead - """ - PlugIn.__init__(self) - self.DBG_LINE='socket' - self._exported_methods=[self.send,self.disconnect] - self._server, self.use_srv = server, use_srv - - def srv_lookup(self, server): - " SRV resolver. Takes server=(host, port) as argument. Returns new (host, port) pair " - if HAVE_DNSPYTHON or HAVE_PYDNS: - host, port = server - possible_queries = ['_xmpp-client._tcp.' + host] - - for query in possible_queries: - try: - if HAVE_DNSPYTHON: - answers = [x for x in dns.resolver.query(query, 'SRV')] - if answers: - host = str(answers[0].target) - port = int(answers[0].port) - break - elif HAVE_PYDNS: - # ensure we haven't cached an old configuration - DNS.DiscoverNameServers() - response = DNS.Request().req(query, qtype='SRV') - answers = response.answers - if len(answers) > 0: - # ignore the priority and weight for now - _, _, port, host = answers[0]['data'] - del _ - port = int(port) - break - except: - self.DEBUG('An error occurred while looking up %s' % query, 'warn') - server = (host, port) - else: - self.DEBUG("Could not load one of the supported DNS libraries (dnspython or pydns). SRV records will not be queried and you may need to set custom hostname/port for some servers to be accessible.\n",'warn') - # end of SRV resolver - return server - - def plugin(self, owner): - """ Fire up connection. Return non-empty string on success. - Also registers self.disconnected method in the owner's dispatcher. - Called internally. """ - if not self._server: self._server=(self._owner.Server,5222) - if self.use_srv: server=self.srv_lookup(self._server) - else: server=self._server - if not self.connect(server): return - self._owner.Connection=self - self._owner.RegisterDisconnectHandler(self.disconnected) - return 'ok' - - def getHost(self): - """ Return the 'host' value that is connection is [will be] made to.""" - return self._server[0] - def getPort(self): - """ Return the 'port' value that is connection is [will be] made to.""" - return self._server[1] - - def connect(self,server=None): - """ Try to connect to the given host/port. Does not lookup for SRV record. - Returns non-empty string on success. """ - try: - if not server: server=self._server - self._sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self._sock.connect((server[0], int(server[1]))) - self._send=self._sock.sendall - self._recv=self._sock.recv - self.DEBUG("Successfully connected to remote host %s"%`server`,'start') - return 'ok' - except socket.error, (errno, strerror): - self.DEBUG("Failed to connect to remote host %s: %s (%s)"%(`server`, strerror, errno),'error') - except: pass - - def plugout(self): - """ Disconnect from the remote server and unregister self.disconnected method from - the owner's dispatcher. """ - self._sock.close() - if self._owner.__dict__.has_key('Connection'): - del self._owner.Connection - self._owner.UnregisterDisconnectHandler(self.disconnected) - - def receive(self): - """ Reads all pending incoming data. - In case of disconnection calls owner's disconnected() method and then raises IOError exception.""" - try: received = self._recv(BUFLEN) - except socket.sslerror,e: - self._seen_data=0 - if e[0]==socket.SSL_ERROR_WANT_READ: return '' - if e[0]==socket.SSL_ERROR_WANT_WRITE: return '' - self.DEBUG('Socket error while receiving data','error') - sys.exc_clear() - self._owner.disconnected() - raise IOError("Disconnected from server") - except: received = '' - - while self.pending_data(0): - try: add = self._recv(BUFLEN) - except: add='' - received +=add - if not add: break - - if len(received): # length of 0 means disconnect - self._seen_data=1 - self.DEBUG(received,'got') - if hasattr(self._owner, 'Dispatcher'): - self._owner.Dispatcher.Event('', DATA_RECEIVED, received) - else: - self.DEBUG('Socket error while receiving data','error') - self._owner.disconnected() - raise IOError("Disconnected from server") - return received - - def send(self,raw_data): - """ Writes raw outgoing data. Blocks until done. - If supplied data is unicode string, encodes it to utf-8 before send.""" - if type(raw_data)==type(u''): raw_data = raw_data.encode('utf-8') - elif type(raw_data)<>type(''): raw_data = ustr(raw_data).encode('utf-8') - try: - self._send(raw_data) - # Avoid printing messages that are empty keepalive packets. - if raw_data.strip(): - self.DEBUG(raw_data,'sent') - if hasattr(self._owner, 'Dispatcher'): # HTTPPROXYsocket will send data before we have a Dispatcher - self._owner.Dispatcher.Event('', DATA_SENT, raw_data) - except: - self.DEBUG("Socket error while sending data",'error') - self._owner.disconnected() - - def pending_data(self,timeout=0): - """ Returns true if there is a data ready to be read. """ - return select.select([self._sock],[],[],timeout)[0] - - def disconnect(self): - """ Closes the socket. """ - self.DEBUG("Closing socket",'stop') - self._sock.close() - - def disconnected(self): - """ Called when a Network Error or disconnection occurs. - Designed to be overidden. """ - self.DEBUG("Socket operation failed",'error') - -DBG_CONNECT_PROXY='CONNECTproxy' -class HTTPPROXYsocket(TCPsocket): - """ HTTP (CONNECT) proxy connection class. Uses TCPsocket as the base class - redefines only connect method. Allows to use HTTP proxies like squid with - (optionally) simple authentication (using login and password). """ - def __init__(self,proxy,server,use_srv=True): - """ Caches proxy and target addresses. - 'proxy' argument is a dictionary with mandatory keys 'host' and 'port' (proxy address) - and optional keys 'user' and 'password' to use for authentication. - 'server' argument is a tuple of host and port - just like TCPsocket uses. """ - TCPsocket.__init__(self,server,use_srv) - self.DBG_LINE=DBG_CONNECT_PROXY - self._proxy=proxy - - def plugin(self, owner): - """ Starts connection. Used interally. Returns non-empty string on success.""" - owner.debug_flags.append(DBG_CONNECT_PROXY) - return TCPsocket.plugin(self,owner) - - def connect(self,dupe=None): - """ Starts connection. Connects to proxy, supplies login and password to it - (if were specified while creating instance). Instructs proxy to make - connection to the target server. Returns non-empty sting on success. """ - if not TCPsocket.connect(self,(self._proxy['host'],self._proxy['port'])): return - self.DEBUG("Proxy server contacted, performing authentification",'start') - connector = ['CONNECT %s:%s HTTP/1.0'%self._server, - 'Proxy-Connection: Keep-Alive', - 'Pragma: no-cache', - 'Host: %s:%s'%self._server, - 'User-Agent: HTTPPROXYsocket/v0.1'] - if self._proxy.has_key('user') and self._proxy.has_key('password'): - credentials = '%s:%s'%(self._proxy['user'],self._proxy['password']) - credentials = base64.encodestring(credentials).strip() - connector.append('Proxy-Authorization: Basic '+credentials) - connector.append('\r\n') - self.send('\r\n'.join(connector)) - try: reply = self.receive().replace('\r','') - except IOError: - self.DEBUG('Proxy suddenly disconnected','error') - self._owner.disconnected() - return - try: proto,code,desc=reply.split('\n')[0].split(' ',2) - except: raise error('Invalid proxy reply') - if code<>'200': - self.DEBUG('Invalid proxy reply: %s %s %s'%(proto,code,desc),'error') - self._owner.disconnected() - return - while reply.find('\n\n') == -1: - try: reply += self.receive().replace('\r','') - except IOError: - self.DEBUG('Proxy suddenly disconnected','error') - self._owner.disconnected() - return - self.DEBUG("Authentification successfull. Jabber server contacted.",'ok') - return 'ok' - - def DEBUG(self,text,severity): - """Overwrites DEBUG tag to allow debug output be presented as "CONNECTproxy".""" - return self._owner.DEBUG(DBG_CONNECT_PROXY,text,severity) - -class TLS(PlugIn): - """ TLS connection used to encrypts already estabilished tcp connection.""" - def PlugIn(self,owner,now=0): - """ If the 'now' argument is true then starts using encryption immidiatedly. - If 'now' in false then starts encryption as soon as TLS feature is - declared by the server (if it were already declared - it is ok). - """ - if owner.__dict__.has_key('TLS'): return # Already enabled. - PlugIn.PlugIn(self,owner) - DBG_LINE='TLS' - if now: return self._startSSL() - if self._owner.Dispatcher.Stream.features: - try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features) - except NodeProcessed: pass - else: self._owner.RegisterHandlerOnce('features',self.FeaturesHandler,xmlns=NS_STREAMS) - self.starttls=None - - def plugout(self,now=0): - """ Unregisters TLS handler's from owner's dispatcher. Take note that encription - can not be stopped once started. You can only break the connection and start over.""" - self._owner.UnregisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS) - self._owner.UnregisterHandler('proceed',self.StartTLSHandler,xmlns=NS_TLS) - self._owner.UnregisterHandler('failure',self.StartTLSHandler,xmlns=NS_TLS) - - def FeaturesHandler(self, conn, feats): - """ Used to analyse server tag for TLS support. - If TLS is supported starts the encryption negotiation. Used internally""" - if not feats.getTag('starttls',namespace=NS_TLS): - self.DEBUG("TLS unsupported by remote server.",'warn') - return - self.DEBUG("TLS supported by remote server. Requesting TLS start.",'ok') - self._owner.RegisterHandlerOnce('proceed',self.StartTLSHandler,xmlns=NS_TLS) - self._owner.RegisterHandlerOnce('failure',self.StartTLSHandler,xmlns=NS_TLS) - self._owner.Connection.send(''%NS_TLS) - raise NodeProcessed - - def pending_data(self,timeout=0): - """ Returns true if there possible is a data ready to be read. """ - return self._tcpsock._seen_data or select.select([self._tcpsock._sock],[],[],timeout)[0] - - def _startSSL(self): - """ Immidiatedly switch socket to TLS mode. Used internally.""" - """ Here we should switch pending_data to hint mode.""" - tcpsock=self._owner.Connection - tcpsock._sslObj = socket.ssl(tcpsock._sock, None, None) - tcpsock._sslIssuer = tcpsock._sslObj.issuer() - tcpsock._sslServer = tcpsock._sslObj.server() - tcpsock._recv = tcpsock._sslObj.read - tcpsock._send = tcpsock._sslObj.write - - tcpsock._seen_data=1 - self._tcpsock=tcpsock - tcpsock.pending_data=self.pending_data - tcpsock._sock.setblocking(0) - - self.starttls='success' - - def StartTLSHandler(self, conn, starttls): - """ Handle server reply if TLS is allowed to process. Behaves accordingly. - Used internally.""" - if starttls.getNamespace()<>NS_TLS: return - self.starttls=starttls.getName() - if self.starttls=='failure': - self.DEBUG("Got starttls response: "+self.starttls,'error') - return - self.DEBUG("Got starttls proceed response. Switching to TLS/SSL...",'ok') - self._startSSL() - self._owner.Dispatcher.PlugOut() - dispatcher.Dispatcher().PlugIn(self._owner) diff --git a/sso/services/jabber/xmppclient.py b/sso/services/jabber/xmppclient.py deleted file mode 100644 index f432dc8..0000000 --- a/sso/services/jabber/xmppclient.py +++ /dev/null @@ -1,184 +0,0 @@ -import time -import xmpp -import random -import hashlib - -class JabberAdmin(): - """ Adds a jabber user to a remote Jabber server """ - - def __init__(self, server, username, password, ip=None): - - self.server = server - self.username = username - self.password = password - - self.jid = xmpp.protocol.JID('%s@%s' % (username, server)) - - if not ip: - self._connect_addr = server - else: - self._connect_addr = ip - - - def __del__(self): - if hasattr(self, '_client'): - self._client.disconnect() - - def connect(self): - if not hasattr(self, '_client'): - client = xmpp.Client(self.jid.getDomain(), debug=[]) - - client.connect(server=('dredd.it', 5222)) - client.auth(self.username, self.password) - client.sendInitPresence() - - self._client = client - - def _construct_iq_req(self, xmlns, node): - n = xmpp.Node('command', attrs={'xmlns': xmlns, 'node': node}) - iq = xmpp.Protocol('iq', self.server, 'set', payload=[n]) - return iq - - def _construct_form(self, xmlns, node, session, values): - - n = xmpp.Node('command', attrs={'xmlns': xmlns, 'node': node, 'sessionid': session}) - x = n.addChild('x', namespace='jabber:x:data', attrs={ 'type': 'submit' }) - - for v in values: - type, var, value = v - x.addChild('field', attrs={'type': type, 'var': var}).addChild('value').addData(value) - - return xmpp.Protocol('iq', self.server, 'set', payload=[n]) - - - def adduser(self, username, password): - try: - self.connect() - except: - return False - # Send request and get the Session ID - resp = self._client.SendAndWaitForResponse(self._construct_iq_req('http://jabber.org/protocol/commands', 'http://jabber.org/protocol/admin#add-user')) - sessionid = resp.getTagAttr('command','sessionid') - - values = [ ('hidden', 'FORM_TYPE', 'http://jabber.org/protocol/admin'), - ('jid-single', 'accountjid', username), - ('text-private', 'password', password), - ('text-private', 'password-verify', password) ] - - iq = self._construct_form('http://jabber.org/protocol/commands', 'http://jabber.org/protocol/admin#add-user', sessionid, values) - - # Send request and pray for the best - resp = self._client.SendAndWaitForResponse(iq) - - if resp.getAttrs()['type'] == "result": - return True - else: - return False - - - def deluser(self, username): - try: - self.connect() - except: - return False - # Send request and get the Session ID - resp = self._client.SendAndWaitForResponse(self._construct_iq_req('http://jabber.org/protocol/commands', 'http://jabber.org/protocol/admin#delete-user')) - sessionid = resp.getTagAttr('command','sessionid') - - values = [ ('hidden', 'FORM_TYPE', 'http://jabber.org/protocol/admin'), - ('jid-multi', 'accountjids', username) ] - - iq = self._construct_form('http://jabber.org/protocol/commands', 'http://jabber.org/protocol/admin#delete-user', sessionid, values) - - # Send request and pray for the best - resp = self._client.SendAndWaitForResponse(iq) - - if resp.getAttrs()['type'] == "result": - return True - else: - return False - - def resetpassword(self, username, password): - try: - self.connect() - except: - return False - # Send request and get the Session ID - resp = self._client.SendAndWaitForResponse(self._construct_iq_req('http://jabber.org/protocol/commands', 'http://jabber.org/protocol/admin#change-user-password')) - sessionid = resp.getTagAttr('command','sessionid') - - values = [ ('hidden', 'FORM_TYPE', 'http://jabber.org/protocol/admin'), - ('jid-single', 'accountjid', username), - ('text-private', 'password', password) ] - - iq = self._construct_form('http://jabber.org/protocol/commands', 'http://jabber.org/protocol/admin#change-user-password', sessionid, values) - - # Send request and pray for the best - resp = self._client.SendAndWaitForResponse(iq) - - if resp.getAttrs()['type'] == "result": - return True - else: - return False - - - def disableuser(self, username): - try: - self.connect() - except: - return False - - if self.resetpassword(username, hashlib.sha1('%s%s' % (username, random.randint(0, 2147483647))).hexdigest()): - return self.kickuser(username) - else: - return False - - def kickuser(self, username): - try: - self.connect() - except: - return False - - # Send request and get the Session ID - resp = self._client.SendAndWaitForResponse(self._construct_iq_req('http://jabber.org/protocol/commands', 'http://jabber.org/protocol/admin#end-user-session')) - sessionid = resp.getTagAttr('command','sessionid') - - values = [ ('hidden', 'FORM_TYPE', 'http://jabber.org/protocol/admin'), - ('jid-single', 'accountjid', username) ] - - iq = self._construct_form('http://jabber.org/protocol/commands', 'http://jabber.org/protocol/admin#end-user-session', sessionid, values) - - # Send request and pray for the best - resp = self._client.SendAndWaitForResponse(iq) - - if resp.getAttrs()['type'] == "result": - return True - else: - return False - - def checkuser(self, username): - try: - self.connect() - except: - return False - # Send request and get the Session ID - resp = self._client.SendAndWaitForResponse(self._construct_iq_req('http://jabber.org/protocol/commands', 'http://jabber.org/protocol/admin#get-user-password')) - sessionid = resp.getTagAttr('command','sessionid') - - values = [ ('hidden', 'FORM_TYPE', 'http://jabber.org/protocol/admin'), - ('jid-single', 'accountjid', username) ] - - iq = self._construct_form('http://jabber.org/protocol/commands', 'http://jabber.org/protocol/admin#get-user-password', sessionid, values) - - # Send request and pray for the best - resp = self._client.SendAndWaitForResponse(iq) - - try: - val = resp.getTag('command').getTag('x').getTag('field', attrs={'label': 'Password'}).getTag('value').getData() - except AttributeError: - return False - - if not val.strip() == '': - return True - - return False From eea7b2390a4b74fccf833125f07fb7bebf0e6986 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Fri, 9 Apr 2010 12:58:07 +0100 Subject: [PATCH 114/116] Removed the check, we want to force deletion --- sso/models.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sso/models.py b/sso/models.py index 1be665d..5b7e89f 100644 --- a/sso/models.py +++ b/sso/models.py @@ -180,8 +180,7 @@ class ServiceAccount(models.Model): @staticmethod def pre_delete_listener( **kwargs ): api = kwargs['instance'].service.api_class - if api.check_user(kwargs['instance'].service_uid): - if not api.delete_user(kwargs['instance'].service_uid): - raise ServiceError('Unable to delete account on related service') + if not api.delete_user(kwargs['instance'].service_uid): + raise ServiceError('Unable to delete account on related service') signals.pre_delete.connect(ServiceAccount.pre_delete_listener, sender=ServiceAccount) From 1fc6b24ea5cf66ecca1ed48a036d1a89d0884094 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Sat, 10 Apr 2010 22:44:54 +0100 Subject: [PATCH 115/116] Further validation done on EVE API key input --- sso/forms.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sso/forms.py b/sso/forms.py index cc1eeb5..2e475a0 100644 --- a/sso/forms.py +++ b/sso/forms.py @@ -16,8 +16,13 @@ class EveAPIForm(forms.Form): description = forms.CharField(max_length=100, required=False) def clean(self): - if not len(self.cleaned_data['api_key']) == 64: + + if not 'api_key' in self.cleaned_data or not len(self.cleaned_data['api_key']) == 64: raise forms.ValidationError("API Key provided is invalid (Not 64 characters long)") + + if not 'user_id' in self.cleaned_data: + raise forms.ValidationError("Please provide a valid User ID") + try: eaccount = EVEAccount.objects.get(api_user_id=self.cleaned_data['user_id']) except EVEAccount.DoesNotExist: From af22b2112b81d904cc69510f5080201075fa9831 Mon Sep 17 00:00:00 2001 From: Andrew Williams Date: Sat, 10 Apr 2010 23:02:16 +0100 Subject: [PATCH 116/116] Fixed return for add_user --- sso/services/jabber/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sso/services/jabber/__init__.py b/sso/services/jabber/__init__.py index 6635b0b..7e6f64a 100644 --- a/sso/services/jabber/__init__.py +++ b/sso/services/jabber/__init__.py @@ -25,7 +25,7 @@ class JabberService(BaseService): if res['res'] == 0: if 'character' in kwargs: self.exec_xmlrpc('set_nickname', user=username, host=settings.JABBER_SERVER, nickname=kwargs['character'].name) - return res['text'].split(" ")[1] + return "%s@%s" % (user=username, host=settings.JABBER_SERVER) else: return False