Provide an API call for the service clients to send user feedback
authorSofia Papagiannaki <papagian@gmail.com>
Tue, 22 May 2012 14:44:14 +0000 (17:44 +0300)
committerSofia Papagiannaki <papagian@gmail.com>
Tue, 22 May 2012 14:44:14 +0000 (17:44 +0300)
* new model Service
* new management commands for handling the services
* remove ASTAKOS_CLOUD_SERVICES setting
* change get_services API call to return
the Service objects
* separate admin from service API calls
* introduce send_feedback service API call

Refs: #2413

15 files changed:
snf-astakos-app/README
snf-astakos-app/astakos/im/api/__init__.py [new file with mode: 0644]
snf-astakos-app/astakos/im/api/admin.py [moved from snf-astakos-app/astakos/im/api.py with 78% similarity]
snf-astakos-app/astakos/im/api/faults.py [moved from snf-astakos-app/astakos/im/faults.py with 100% similarity]
snf-astakos-app/astakos/im/api/service.py [new file with mode: 0644]
snf-astakos-app/astakos/im/context_processors.py
snf-astakos-app/astakos/im/management/commands/listservices.py [new file with mode: 0644]
snf-astakos-app/astakos/im/management/commands/registerservice.py [new file with mode: 0644]
snf-astakos-app/astakos/im/management/commands/renewservicetoken.py [new file with mode: 0644]
snf-astakos-app/astakos/im/management/commands/unregisterservice.py [new file with mode: 0644]
snf-astakos-app/astakos/im/migrations/0009_auto__add_service.py [new file with mode: 0644]
snf-astakos-app/astakos/im/models.py
snf-astakos-app/astakos/im/settings.py
snf-astakos-app/astakos/im/urls.py
snf-astakos-app/conf/20-snf-astakos-app-settings.conf

index 3e02efa..1adf73c 100644 (file)
@@ -59,9 +59,6 @@ ASTAKOS_IM_STATIC_URL               /static/im/
 ASTAKOS_MODERATION_ENABLED          True                                                                            If False and invitations are not enabled newly created user will be automatically accepted
 ASTAKOS_BASEURL                     \http://pithos.dev.grnet.gr                                                     Astakos baseurl
 ASTAKOS_SITENAME                    GRNET Cloud                                                                     Service name that appears in emails
-ASTAKOS_CLOUD_SERVICES              ({'icon': 'home-icon.png', 'id': 'cloud', 'name': 'grnet cloud', 'url': '/'},   Cloud services appear in the horizontal bar
-                                    {'id': 'okeanos', 'name': 'cyclades', 'url': '/okeanos.html'},                  
-                                    {'id': 'pithos', 'name': 'pithos+', 'url': '/ui/'})                             
 ASTAKOS_RECAPTCHA_ENABLED           True                                                                            Enable recaptcha
 ASTAKOS_RECAPTCHA_PUBLIC_KEY                                                                                        Recaptcha public key obtained after registration here: http://recaptcha.net
 ASTAKOS_RECAPTCHA_PRIVATE_KEY                                                                                       Recaptcha private key obtained after registration here: http://recaptcha.net
diff --git a/snf-astakos-app/astakos/im/api/__init__.py b/snf-astakos-app/astakos/im/api/__init__.py
new file mode 100644 (file)
index 0000000..ba70e3a
--- /dev/null
@@ -0,0 +1,90 @@
+# Copyright 2011-2012 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
+from django.http import HttpResponse
+from django.utils import simplejson as json
+
+from astakos.im.models import AstakosUser
+from astakos.im.faults import ItemNotFound
+
+format = ('%a, %d %b %Y %H:%M:%S GMT')
+
+def _get_user_by_username(user_id):
+    try:
+        user = AstakosUser.objects.get(username = user_id)
+    except AstakosUser.DoesNotExist, e:
+        raise ItemNotFound('Invalid userid')
+    else:
+        response = HttpResponse()
+        response.status=200
+        user_info = {'id':user.id,
+                     'username':user.username,
+                     'email':[user.email],
+                     'name':user.realname,
+                     'auth_token_created':user.auth_token_created.strftime(format),
+                     'auth_token_expires':user.auth_token_expires.strftime(format),
+                     'has_credits':user.has_credits,
+                     'enabled':user.is_active,
+                     'groups':[g.name for g in user.groups.all()]}
+        response.content = json.dumps(user_info)
+        response['Content-Type'] = 'application/json; charset=UTF-8'
+        response['Content-Length'] = len(response.content)
+        return response
+
+def _get_user_by_email(email):
+    if not email:
+        raise BadRequest('Email missing')
+    try:
+        user = AstakosUser.objects.get(email = email)
+    except AstakosUser.DoesNotExist, e:
+        raise ItemNotFound('Invalid email')
+    
+    if not user.is_active:
+        raise ItemNotFound('Inactive user')
+    else:
+        response = HttpResponse()
+        response.status=200
+        user_info = {'id':user.id,
+                     'username':user.username,
+                     'email':[user.email],
+                     'enabled':user.is_active,
+                     'name':user.realname,
+                     'auth_token_created':user.auth_token_created.strftime(format),
+                     'auth_token_expires':user.auth_token_expires.strftime(format),
+                     'has_credits':user.has_credits,
+                     'groups':[g.name for g in user.groups.all()],
+                     'user_permissions':[p.codename for p in user.user_permissions.all()]}
+        response.content = json.dumps(user_info)
+        response['Content-Type'] = 'application/json; charset=UTF-8'
+        response['Content-Length'] = len(response.content)
+        return response
\ No newline at end of file
similarity index 78%
rename from snf-astakos-app/astakos/im/api.py
rename to snf-astakos-app/astakos/im/api/admin.py
index 98d2eb9..6b23464 100644 (file)
@@ -39,18 +39,18 @@ from traceback import format_exc
 from time import time, mktime
 from urllib import quote
 from urlparse import urlparse
+from collections import defaultdict
 
 from django.conf import settings
 from django.http import HttpResponse
 from django.utils import simplejson as json
 from django.core.urlresolvers import reverse
 
-from astakos.im.faults import BadRequest, Unauthorized, InternalServerError, \
-Fault, ItemNotFound, Forbidden
-from astakos.im.models import AstakosUser
-from astakos.im.settings import CLOUD_SERVICES, INVITATIONS_ENABLED, COOKIE_NAME, \
-EMAILCHANGE_ENABLED
+from astakos.im.api.faults import *
+from astakos.im.models import AstakosUser, Service
+from astakos.im.settings import INVITATIONS_ENABLED, COOKIE_NAME, EMAILCHANGE_ENABLED
 from astakos.im.util import epoch
+from astakos.im.api import _get_user_by_email, _get_user_by_username
 
 logger = logging.getLogger(__name__)
 format = ('%a, %d %b %Y %H:%M:%S GMT')
@@ -84,6 +84,9 @@ def api_method(http_method=None, token_required=False, perms=None):
                         raise Unauthorized('Access denied')
                     try:
                         user = AstakosUser.objects.get(auth_token=x_auth_token)
+                        ## Check if the token has expired.
+                        #if (time() - mktime(user.auth_token_expires.timetuple())) > 0:
+                        #    raise Unauthorized('Authentication expired')
                         if not user.has_perms(perms):
                             raise Forbidden('Unauthorized request')
                     except AstakosUser.DoesNotExist, e:
@@ -173,7 +176,9 @@ def authenticate(request, user=None):
 @api_method(http_method='GET')
 def get_services(request):
     callback = request.GET.get('callback', None)
-    data = json.dumps(CLOUD_SERVICES)
+    services = Service.objects.all()
+    data = tuple({'name':s.name, 'url':s.url, 'icon':s.icon} for s in services)
+    data = json.dumps(data)
     mimetype = 'application/json'
 
     if callback:
@@ -225,7 +230,7 @@ def get_menu(request, with_extra_links=False, with_signout=True):
 
     return HttpResponse(content=data, mimetype=mimetype)
 
-@api_method(http_method='GET', token_required=True, perms=['im.can_access_userinfo'])
+@api_method(http_method='GET', token_required=True)
 def get_user_by_email(request, user=None):
     # Normal Response Codes: 200
     # Error Response Codes: internalServerError (500)
@@ -234,35 +239,9 @@ def get_user_by_email(request, user=None):
     #                       forbidden (403)
     #                       itemNotFound (404)
     email = request.GET.get('name')
-    if not email:
-        raise BadRequest('Email missing')
-    try:
-        user = AstakosUser.objects.get(email = email)
-    except AstakosUser.DoesNotExist, e:
-        raise ItemNotFound('Invalid email')
-    
-    if not user.is_active:
-        raise ItemNotFound('Inactive user')
-    else:
-        response = HttpResponse()
-        response.status=200
-        user_info = {'id':user.id,
-                     'username':user.username,
-                     'email':[user.email],
-                     'enabled':user.is_active,
-                     'name':user.realname,
-                     'auth_token_created':user.auth_token_created.strftime(format),
-                     'auth_token_expires':user.auth_token_expires.strftime(format),
-                     'has_credits':user.has_credits,
-                     'groups':[g.name for g in user.groups.all()],
-                     'user_permissions':[p.codename for p in user.user_permissions.all()],
-                     'group_permissions': list(user.get_group_permissions())}
-        response.content = json.dumps(user_info)
-        response['Content-Type'] = 'application/json; charset=UTF-8'
-        response['Content-Length'] = len(response.content)
-        return response
+    return _get_user_by_email(email)
 
-@api_method(http_method='GET', token_required=True, perms=['can_access_userinfo'])
+@api_method(http_method='GET', token_required=True)
 def get_user_by_username(request, user_id, user=None):
     # Normal Response Codes: 200
     # Error Response Codes: internalServerError (500)
@@ -270,23 +249,4 @@ def get_user_by_username(request, user_id, user=None):
     #                       unauthorised (401)
     #                       forbidden (403)
     #                       itemNotFound (404)
-    try:
-        user = AstakosUser.objects.get(username = user_id)
-    except AstakosUser.DoesNotExist, e:
-        raise ItemNotFound('Invalid userid')
-    else:
-        response = HttpResponse()
-        response.status=200
-        user_info = {'id':user.id,
-                     'username':user.username,
-                     'email':[user.email],
-                     'name':user.realname,
-                     'auth_token_created':user.auth_token_created.strftime(format),
-                     'auth_token_expires':user.auth_token_expires.strftime(format),
-                     'has_credits':user.has_credits,
-                     'enabled':user.is_active,
-                     'groups':[g.name for g in user.groups.all()]}
-        response.content = json.dumps(user_info)
-        response['Content-Type'] = 'application/json; charset=UTF-8'
-        response['Content-Length'] = len(response.content)
-        return response
\ No newline at end of file
+    return _get_user_by_username(user_id)
\ No newline at end of file
diff --git a/snf-astakos-app/astakos/im/api/service.py b/snf-astakos-app/astakos/im/api/service.py
new file mode 100644 (file)
index 0000000..fd2be50
--- /dev/null
@@ -0,0 +1,151 @@
+# Copyright 2011-2012 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
+import logging
+import urllib
+
+from functools import wraps
+from traceback import format_exc
+from time import time, mktime
+from urllib import quote
+from urlparse import urlparse
+from collections import defaultdict
+
+from django.conf import settings
+from django.http import HttpResponse
+from django.core.urlresolvers import reverse
+from django.views.decorators.csrf import csrf_exempt
+
+from astakos.im.api.faults import *
+from astakos.im.models import AstakosUser, Service
+from astakos.im.settings import INVITATIONS_ENABLED, COOKIE_NAME, EMAILCHANGE_ENABLED
+from astakos.im.util import epoch
+from astakos.im.forms import FeedbackForm
+from astakos.im.functions import send_feedback as send_feedback_func, SendMailError
+
+logger = logging.getLogger(__name__)
+
+def render_fault(request, fault):
+    if isinstance(fault, InternalServerError) and settings.DEBUG:
+        fault.details = format_exc(fault)
+
+    request.serialization = 'text'
+    data = fault.message + '\n'
+    if fault.details:
+        data += '\n' + fault.details
+    response = HttpResponse(data, status=fault.code)
+    response['Content-Length'] = len(response.content)
+    return response
+
+def api_method(http_method=None, token_required=False):
+    """Decorator function for views that implement an API method."""
+    def decorator(func):
+        @wraps(func)
+        def wrapper(request, *args, **kwargs):
+            try:
+                if http_method and request.method != http_method:
+                    raise BadRequest('Method not allowed.')
+                x_auth_token = request.META.get('HTTP_X_AUTH_TOKEN')
+                if token_required:
+                    if not x_auth_token:
+                        raise Unauthorized('Access denied')
+                    try:
+                        service = Service.objects.get(auth_token=x_auth_token)
+                        
+                        # Check if the token has expired.
+                        if (time() - mktime(service.auth_token_expires.timetuple())) > 0:
+                            raise Unauthorized('Authentication expired')
+                    except Service.DoesNotExist, e:
+                        raise Unauthorized('Invalid X-Auth-Token')
+                response = func(request, *args, **kwargs)
+                return response
+            except Fault, fault:
+                return render_fault(request, fault)
+            except BaseException, e:
+                logger.exception('Unexpected error: %s' % e)
+                fault = InternalServerError('Unexpected error')
+                return render_fault(request, fault)
+        return wrapper
+    return decorator
+
+@api_method(http_method='GET', token_required=True)
+def get_user_by_email(request, user=None):
+    # Normal Response Codes: 200
+    # Error Response Codes: internalServerError (500)
+    #                       badRequest (400)
+    #                       unauthorised (401)
+    #                       forbidden (403)
+    #                       itemNotFound (404)
+    email = request.GET.get('name')
+    return _get_user_by_email(email)
+
+@api_method(http_method='GET', token_required=True)
+def get_user_by_username(request, user_id, user=None):
+    # Normal Response Codes: 200
+    # Error Response Codes: internalServerError (500)
+    #                       badRequest (400)
+    #                       unauthorised (401)
+    #                       forbidden (403)
+    #                       itemNotFound (404)
+    return _get_user_by_username(user_id)
+
+@csrf_exempt
+@api_method(http_method='POST', token_required=True)
+def send_feedback(request, email_template_name='im/feedback_mail.txt'):
+    # Normal Response Codes: 200
+    # Error Response Codes: internalServerError (500)
+    #                       badRequest (400)
+    #                       unauthorised (401)
+    auth_token = request.POST.get('auth', '')
+    if not auth_token:
+        raise BadRequest('Missing user authentication')
+    
+    user  = None
+    try:
+        user = AstakosUser.objects.get(auth_token=auth_token)
+    except:
+        pass
+    
+    if not user:
+        raise BadRequest('Invalid user authentication')
+    
+    form = FeedbackForm(request.POST)
+    if not form.is_valid():
+        raise BadRequest('Invalid data')
+    
+    msg = form.cleaned_data['feedback_msg']
+    data = form.cleaned_data['feedback_data']
+    send_feedback_func(msg, data, user, email_template_name)
+    response = HttpResponse(status=200)
+    response['Content-Length'] = len(response.content)
+    return response
\ No newline at end of file
index 01e7ff7..94499f4 100644 (file)
@@ -33,7 +33,7 @@
 
 from astakos.im.settings import IM_MODULES, INVITATIONS_ENABLED, IM_STATIC_URL, \
         COOKIE_NAME, LOGIN_MESSAGES, PROFILE_EXTRA_LINKS
-from astakos.im.api import get_menu
+from astakos.im.api.admin import get_menu
 from astakos.im.util import get_query
 
 from django.conf import settings
diff --git a/snf-astakos-app/astakos/im/management/commands/listservices.py b/snf-astakos-app/astakos/im/management/commands/listservices.py
new file mode 100644 (file)
index 0000000..0408bc2
--- /dev/null
@@ -0,0 +1,74 @@
+# Copyright 2012 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
+from optparse import make_option
+
+from django.core.management.base import BaseCommand, CommandError
+
+from astakos.im.models import Service
+
+class Command(BaseCommand):
+    help = "List g"
+    
+    option_list = BaseCommand.option_list + (
+        make_option('-c',
+            action='store_true',
+            dest='csv',
+            default=False,
+            help="Use pipes to separate values"),
+    )
+    
+    def handle(self, *args, **options):
+        if args:
+            raise CommandError("Command doesn't accept any arguments")
+        
+        services = Service.objects.all()
+        
+        labels = ('id', 'name', 'url', 'icon')
+        columns = (3, 12, 40, 40)
+        
+        if not options['csv']:
+            line = ' '.join(l.rjust(w) for l, w in zip(labels, columns))
+            self.stdout.write(line + '\n')
+            sep = '-' * len(line)
+            self.stdout.write(sep + '\n')
+        
+        for service in services:
+            fields = (str(service.id), service.name, service.url, service.icon)
+            
+            if options['csv']:
+                line = '|'.join(fields)
+            else:
+                line = ' '.join(f.rjust(w) for f, w in zip(fields, columns))
+            
+            self.stdout.write(line.encode('utf8') + '\n')
diff --git a/snf-astakos-app/astakos/im/management/commands/registerservice.py b/snf-astakos-app/astakos/im/management/commands/registerservice.py
new file mode 100644 (file)
index 0000000..61b864d
--- /dev/null
@@ -0,0 +1,53 @@
+# Copyright 2012 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
+from django.core.management.base import BaseCommand, CommandError
+
+from astakos.im.models import Service
+
+class Command(BaseCommand):
+    args = "<name> <url> [<icon>]"
+    help = "Register a service"
+    
+    def handle(self, *args, **options):
+        if len(args) < 2:
+            raise CommandError("Invalid number of arguments")
+        
+        service = Service(name=args[0], url=args[1])
+        if len(args) == 3:
+            service.icon = args[2]
+        try:
+            service.save()
+            self.stdout.write('Service created with token: %s\n' % service.auth_token)
+        except Exception, e:
+            raise CommandError(e)
\ No newline at end of file
diff --git a/snf-astakos-app/astakos/im/management/commands/renewservicetoken.py b/snf-astakos-app/astakos/im/management/commands/renewservicetoken.py
new file mode 100644 (file)
index 0000000..19e889a
--- /dev/null
@@ -0,0 +1,54 @@
+# Copyright 2012 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
+from django.core.management.base import BaseCommand, CommandError
+
+from astakos.im.models import Service
+
+class Command(BaseCommand):
+    args = "<name>"
+    help = "Renew service token"
+    
+    def handle(self, *args, **options):
+        if len(args) != 1:
+            raise CommandError("Invalid number of arguments")
+        
+        try:
+            service = Service.objects.get(name=args[0])
+            service.renew_token()
+            service.save()
+            self.stdout.write('New service token: %s\n' % service.auth_token)
+        except Service.DoesNotExist:
+            raise CommandError("Invalid service name")
+        except Exception, e:
+            raise CommandError(e)
\ No newline at end of file
diff --git a/snf-astakos-app/astakos/im/management/commands/unregisterservice.py b/snf-astakos-app/astakos/im/management/commands/unregisterservice.py
new file mode 100644 (file)
index 0000000..e071b62
--- /dev/null
@@ -0,0 +1,50 @@
+# Copyright 2012 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
+from django.core.management.base import BaseCommand, CommandError
+
+from astakos.im.models import Service
+
+class Command(BaseCommand):
+    args = "<name>"
+    help = "Unregister a service"
+    
+    def handle(self, *args, **options):
+        if len(args) < 1:
+            raise CommandError("Invalid number of arguments")
+        
+        try:
+            service = Service.objects.get(name=args[0])
+            service.delete()
+        except Service.DoesNotExist, e:
+            raise CommandError(e)
\ No newline at end of file
diff --git a/snf-astakos-app/astakos/im/migrations/0009_auto__add_service.py b/snf-astakos-app/astakos/im/migrations/0009_auto__add_service.py
new file mode 100644 (file)
index 0000000..3c5931a
--- /dev/null
@@ -0,0 +1,122 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        
+        # Adding model 'Service'
+        db.create_table('im_service', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)),
+            ('url', self.gf('django.db.models.fields.URLField')(max_length=200)),
+            ('icon', self.gf('django.db.models.fields.FilePathField')(max_length=100)),
+            ('auth_token', self.gf('django.db.models.fields.CharField')(max_length=32, null=True, blank=True)),
+            ('auth_token_created', self.gf('django.db.models.fields.DateTimeField')(null=True)),
+            ('auth_token_expires', self.gf('django.db.models.fields.DateTimeField')(null=True)),
+        ))
+        db.send_create_signal('im', ['Service'])
+
+
+    def backwards(self, orm):
+        
+        # Deleting model 'Service'
+        db.delete_table('im_service')
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'im.approvalterms': {
+            'Meta': {'object_name': 'ApprovalTerms'},
+            'date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 5, 21, 15, 21, 13, 352838)', 'db_index': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'location': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'im.astakosuser': {
+            'Meta': {'object_name': 'AstakosUser', '_ormbases': ['auth.User']},
+            'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
+            'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'invitations': ('django.db.models.fields.IntegerField', [], {'default': '100'}),
+            'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'level': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
+            'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'})
+        },
+        'im.emailchange': {
+            'Meta': {'object_name': 'EmailChange'},
+            'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
+            'requested_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 5, 21, 15, 21, 13, 354390)'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchange_user'", 'unique': 'True', 'to': "orm['im.AstakosUser']"})
+        },
+        'im.invitation': {
+            'Meta': {'object_name': 'Invitation'},
+            'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
+            'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}),
+            'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
+        },
+        'im.service': {
+            'Meta': {'object_name': 'Service'},
+            'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
+            'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'icon': ('django.db.models.fields.FilePathField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+            'url': ('django.db.models.fields.URLField', [], {'max_length': '200'})
+        }
+    }
+
+    complete_apps = ['im']
index 739849d..4fa35d4 100644 (file)
@@ -331,6 +331,32 @@ class EmailChange(models.Model):
         expiration_date = timedelta(days=EMAILCHANGE_ACTIVATION_DAYS)
         return self.requested_at + expiration_date < datetime.now()
 
+class Service(models.Model):
+    name = models.CharField('Name', max_length=255, unique=True)
+    url = models.FilePathField()
+    icon = models.FilePathField(blank=True)
+    auth_token = models.CharField('Authentication Token', max_length=32,
+                                  null=True, blank=True)
+    auth_token_created = models.DateTimeField('Token creation date', null=True)
+    auth_token_expires = models.DateTimeField('Token expiration date', null=True)
+    
+    def save(self, **kwargs):
+        if not self.id:
+            self.renew_token()
+        self.full_clean()
+        super(Service, self).save(**kwargs)
+    
+    def renew_token(self):
+        md5 = hashlib.md5()
+        md5.update(self.name.encode('ascii', 'ignore'))
+        md5.update(self.url.encode('ascii', 'ignore'))
+        md5.update(asctime())
+
+        self.auth_token = b64encode(md5.digest())
+        self.auth_token_created = datetime.now()
+        self.auth_token_expires = self.auth_token_created + \
+                                  timedelta(hours=AUTH_TOKEN_DURATION)
+
 def create_astakos_user(u):
     try:
         AstakosUser.objects.get(user_ptr=u.pk)
index 4adfa3e..628f4c7 100644 (file)
@@ -47,12 +47,6 @@ BASEURL = getattr(settings, 'ASTAKOS_BASEURL', 'http://pithos.dev.grnet.gr')
 # Set service name
 SITENAME = getattr(settings, 'ASTAKOS_SITENAME', 'GRNET Cloud')
 
-# Set cloud services appear in the horizontal bar
-CLOUD_SERVICES = getattr(settings, 'ASTAKOS_CLOUD_SERVICES', (
-        { 'url':'/', 'name':'grnet cloud', 'id':'cloud', 'icon':'home-icon.png' },
-        { 'url':'/okeanos.html', 'name':'cyclades', 'id':'okeanos' },
-        { 'url':'/ui/', 'name':'pithos+', 'id':'pithos' }))
-
 # Set recaptcha keys
 RECAPTCHA_PUBLIC_KEY = getattr(settings, 'ASTAKOS_RECAPTCHA_PUBLIC_KEY', '')
 RECAPTCHA_PRIVATE_KEY = getattr(settings, 'ASTAKOS_RECAPTCHA_PRIVATE_KEY', '')
index 1b76b58..064daa4 100644 (file)
@@ -93,11 +93,18 @@ if 'twitter' in IM_MODULES:
         url(r'^login/twitter/authenticated/?$', 'twitter.authenticated')
     )
 
-urlpatterns += patterns('astakos.im.api',
+urlpatterns += patterns('astakos.im.api.admin',
     url(r'^authenticate/?$', 'authenticate_old'),
-    url(r'^authenticate/v2/?$', 'authenticate'),
+    #url(r'^authenticate/v2/?$', 'authenticate'),
     url(r'^get_services/?$', 'get_services'),
     url(r'^get_menu/?$', 'get_menu'),
-    url(r'^v2.0/users/?$', 'get_user_by_email'),
-    url(r'^v2.0/users/(?P<user_id>.+?)/?$', 'get_user_by_username'),
+    url(r'^admin/api/v2.0/users/?$', 'get_user_by_email'),
+    url(r'^admin/api/v2.0/users/(?P<user_id>.+?)/?$', 'get_user_by_username'),
 )
+
+urlpatterns += patterns('astakos.im.api.service',
+    #url(r'^service/api/v2.0/tokens/(?P<token_id>.+?)/?$', 'validate_token'),
+    url(r'^service/api/v2.0/feedback/?$', 'send_feedback'),
+    url(r'^service/api/v2.0/users/?$', 'get_user_by_email'),
+    url(r'^service/api/v2.0/users/(?P<user_id>.+?)/?$', 'get_user_by_username'),
+)
\ No newline at end of file
index 228c58c..5aeaf34 100644 (file)
 # Set service name
 #ASTAKOS_SITENAME = 'GRNET Cloud'
 
-# Set cloud services appear in the horizontal bar
-#ASTAKOS_CLOUD_SERVICES = (
-#        { 'url':'/', 'name':'grnet cloud', 'id':'cloud', 'icon':'home-icon.png' },
-#        { 'url':'/okeanos.html', 'name':'~okeanos', 'id':'okeanos' },
-#        { 'url':'/ui/', 'name':'pithos+', 'id':'pithos' })
-#
-
 # Set recaptcha keys
 # http://www.google.com/recaptcha/whyrecaptcha 
 #ASTAKOS_RECAPTCHA_PUBLIC_KEY = ''