Пример #1
0
 def cmd_load(self, subcontext, page=None):
     if page is None:
         page = self.page
     context = page + ':' + subcontext
     state = self.macro.get_tracform_state(context)
     for name, value in json.loads(state or '{}').iteritems():
         self.env[context + ':' + name] = value
         if self.subcontext is not None:
             self.env[self.subcontext + ':' + name] = value
Пример #2
0
 def cmd_load(self, subcontext, page=None):
     if page is None:
         page = self.page
     context = page + ':' + subcontext
     state = self.macro.get_tracform_state(context)
     for name, value in json.loads(state or '{}').iteritems():
         self.env[context + ':' + name] = value
         if self.subcontext is not None:
             self.env[self.subcontext + ':' + name] = value
Пример #3
0
 def process_request(self, req):
     req.perm.require('FORM_EDIT_VAL')
     try:
         self.log.debug('UPDATE ARGS:' + str(req.args))
         args = dict(req.args)
         backpath = args.pop('__backpath__', None)
         context = json.loads(
             unquote_plus(args.pop('__context__', '[null, null, null]')))
         if None in context:
             # TRANSLATOR: HTTP error message
             raise HTTPBadRequest(_("__context__ is required"))
         basever = args.pop('__basever__', None)
         keep_history = args.pop('__keep_history__', None)
         track_fields = args.pop('__track_fields__', None)
         args.pop('__FORM_TOKEN', None)  # Ignore.
         # wipe not JSON serializable arguments
         rejargs = []
         for key, value in args.iteritems():
             try:
                 len(value)
             except AttributeError:
                 rejargs.append(key)
                 pass
         for key in rejargs:
             args.pop(key)
         who = req.authname
         result = json.dumps(args, separators=(',', ':'))
         self.save_tracform(context,
                            result,
                            who,
                            basever,
                            keep_history=keep_history,
                            track_fields=track_fields)
         buffer = 'OK'
         if backpath is not None:
             req.send_response(302)
             req.send_header('Content-Type', 'text/plain')
             req.send_header('Location', backpath)
             req.send_header('Content-Length', str(len(buffer)))
             req.end_headers()
             req.write(buffer)
         else:
             req.send_response(200)
             req.send_header('Content-Type', 'text/plain')
             req.send_header('Content-Length', str(len(buffer)))
             req.end_headers()
             req.write(buffer)
     except Exception, e:
         buffer = str(e)
         req.send_response(500)
         req.send_header('Content-type', 'text/plain')
         req.send_header('Content-Length', str(len(buffer)))
         req.end_headers()
         req.write(buffer)
Пример #4
0
 def process_request(self, req):
     req.perm.require('FORM_EDIT_VAL')
     try:
         self.log.debug('UPDATE ARGS:' + str(req.args))
         args = dict(req.args)
         backpath = args.pop('__backpath__', None)
         context = json.loads(unquote_plus(
                       args.pop('__context__', '[null, null, null]')))
         basever = args.pop('__basever__', None)
         keep_history = args.pop('__keep_history__', None)
         track_fields = args.pop('__track_fields__', None)
         args.pop('__FORM_TOKEN', None)  # Ignore.
         if context is None:
             # TRANSLATOR: HTTP error message
             raise HTTPBadRequest(_("__context__ is required"))
         # wipe not JSON serializable arguments
         rejargs = []
         for key, value in args.iteritems():
             try:
                 len(value)
             except AttributeError:
                 rejargs.append(key)
                 pass
         for key in rejargs:
             args.pop(key)
         who = req.authname
         result = json.dumps(args, separators=(',', ':'))
         self.save_tracform(context, result, who, basever,
                             keep_history=keep_history,
                             track_fields=track_fields)
         buffer = 'OK'
         if backpath is not None:
             req.send_response(302)
             req.send_header('Content-Type', 'text/plain')
             req.send_header('Location', backpath)
             req.send_header('Content-Length', str(len(buffer)))
             req.end_headers()
             req.write(buffer)
         else:
             req.send_response(200)
             req.send_header('Content-Type', 'text/plain')
             req.send_header('Content-Length', str(len(buffer)))
             req.end_headers()
             req.write(buffer)
     except Exception, e:
         buffer = str(e)
         req.send_response(500)
         req.send_header('Content-type', 'text/plain')
         req.send_header('Content-Length', str(len(buffer)))
         req.end_headers()
         req.write(buffer)
    def _request(self, path, query={}, ignore_cache=False, cache_timeout=None):
        MOZILLIANS_API_BASE_URL = constance.config.MOZILLIANS_API_BASE_URL
        MOZILLIANS_API_APPNAME = constance.config.MOZILLIANS_API_APPNAME
        MOZILLIANS_API_KEY = constance.config.MOZILLIANS_API_KEY
        MOZILLIANS_API_CACHE_KEY_PREFIX = constance.config.MOZILLIANS_API_CACHE_KEY_PREFIX
        MOZILLIANS_API_CACHE_TIMEOUT = constance.config.MOZILLIANS_API_CACHE_TIMEOUT

        endpoint = self.endpoint or MOZILLIANS_API_BASE_URL
        app_name = self.name or MOZILLIANS_API_APPNAME
        app_key = self.key or MOZILLIANS_API_KEY
        if cache_timeout is None:
            cache_timeout = MOZILLIANS_API_CACHE_TIMEOUT

        if not app_name or not app_key:
            logging.warning('Missing Mozillians app name or key')
            return None

        query['app_name'] = app_name
        query['app_key'] = app_key

        base = urlparse.urljoin(endpoint.rstrip('/') + '/', path.lstrip('/'))
        qs = urllib.urlencode(
            sorted([(key, value) for key, value in query.iteritems()]))
        url = '%s?%s' % (
            base,
            qs,
        )

        cache_key = '%s:%s' % (MOZILLIANS_API_CACHE_KEY_PREFIX,
                               hashlib.md5(url.encode('utf-8')).hexdigest())

        content = None if ignore_cache else cache.get(cache_key)

        if not content:
            try:
                request = urllib2.Request(url)
                response = urllib2.urlopen(request)
                content = response.read()
                cache.set(cache_key, content, cache_timeout)
            except urllib2.URLError as e:
                logging.error('Mozillians request failed:\n - %s\n - %s', e,
                              url)
                return None

        try:
            return json.loads(content)
        except ValueError as e:
            logging.error('Parsing Mozillians response failed:\n - %s\n - %s',
                          e, url)
            return None
Пример #6
0
    def _postprocess(self, response):
        """
        Makes modifications to the response object before returning it.

        If you don't want to throw exceptions, or parse json, override this
        and just return `response`
        """
        response.raise_for_status()

        # For some reason, this doesn't work.
        #content = json.load(response)
        content = json.loads(response.content)

        if content['ok'] == '0' or content['ok'] == 0:
            raise GlitchError(content, response)

        return content
Пример #7
0
 def process_request(self, req):
     req.perm.require('FORM_EDIT_VAL')
     try:
         self.log.debug('UPDATE ARGS:' + str(req.args))
         args = dict(req.args)
         backpath = args.pop('__backpath__', None)
         context = json.loads(
             unquote_plus(args.pop('__context__', None)) or \
             '(None, None, None)')
         basever = args.pop('__basever__', None)
         keep_history = args.pop('__keep_history__', None)
         track_fields = args.pop('__track_fields__', None)
         args.pop('__FORM_TOKEN', None)  # Ignore.
         if context is None:
             # TRANSLATOR: HTTP error message
             raise HTTPBadRequest(_("__context__ is required"))
         who = req.authname
         result = json.dumps(args, separators=(',', ':'))
         self.save_tracform(context,
                            result,
                            who,
                            basever,
                            keep_history=keep_history,
                            track_fields=track_fields)
         buffer = 'OK'
         if backpath is not None:
             req.send_response(302)
             req.send_header('Content-Type', 'text/plain')
             req.send_header('Location', backpath)
             req.send_header('Content-Length', str(len(buffer)))
             req.end_headers()
             req.write(buffer)
         else:
             req.send_response(200)
             req.send_header('Content-Type', 'text/plain')
             req.send_header('Content-Length', str(len(buffer)))
             req.end_headers()
             req.write(buffer)
     except Exception, e:
         buffer = str(e)
         req.send_response(500)
         req.send_header('Content-type', 'text/plain')
         req.send_header('Content-Length', str(len(buffer)))
         req.end_headers()
         req.write(buffer)
Пример #8
0
 def _render_fields(self, form_id, state):
     fields = json.loads(state is not None and state or '{}')
     rendered = []
     for name, value in fields.iteritems():
         if value == 'on':
            value = _("checked (checkbox)")
         elif value == '':
            value = _("empty (text field)")
         else:
            value = '\'' + value + '\''
         author, time = self.get_tracform_fieldinfo(form_id, name)
         rendered.append(
             {'name': name, 'value': value,
              'author': tag.span(tag_("by %(author)s", author=author),
                                 class_='author'),
              'time': time is not None and tag.span(
                      format_datetime(time), class_='date') or None})
     return rendered
Пример #9
0
    def _request(self, path, query={}, ignore_cache=False, cache_timeout=None):
        MOZILLIANS_API_BASE_URL = constance.config.MOZILLIANS_API_BASE_URL
        MOZILLIANS_API_APPNAME = constance.config.MOZILLIANS_API_APPNAME
        MOZILLIANS_API_KEY = constance.config.MOZILLIANS_API_KEY
        MOZILLIANS_API_CACHE_KEY_PREFIX = constance.config.MOZILLIANS_API_CACHE_KEY_PREFIX
        MOZILLIANS_API_CACHE_TIMEOUT = constance.config.MOZILLIANS_API_CACHE_TIMEOUT

        endpoint = self.endpoint or MOZILLIANS_API_BASE_URL
        app_name = self.name or MOZILLIANS_API_APPNAME
        app_key = self.key or MOZILLIANS_API_KEY
        if cache_timeout is None:
            cache_timeout = MOZILLIANS_API_CACHE_TIMEOUT

        if not app_name or not app_key:
            logging.warning('Missing Mozillians app name or key')
            return None

        query['app_name'] = app_name
        query['app_key'] = app_key

        base = urlparse.urljoin(endpoint.rstrip('/') + '/', path.lstrip('/'))
        qs = urllib.urlencode(sorted([(key, value) for key, value in query.iteritems()]))
        url = '%s?%s' % (base, qs,)

        cache_key = '%s:%s' % (MOZILLIANS_API_CACHE_KEY_PREFIX,
                               hashlib.md5(url.encode('utf-8')).hexdigest())

        content = None if ignore_cache else cache.get(cache_key)

        if not content:
            try:
                request = urllib2.Request(url)
                response = urllib2.urlopen(request)
                content = response.read()
                cache.set(cache_key, content, cache_timeout)
            except urllib2.URLError as e:
                logging.error('Mozillians request failed:\n - %s\n - %s', e, url)
                return None

        try:
            return json.loads(content)
        except ValueError as e:
            logging.error('Parsing Mozillians response failed:\n - %s\n - %s', e, url)
            return None
Пример #10
0
    def _method(self, method_name, args=None, additional_timeout=0, retry=0):
        """
        Makes post-request to vk api witch burst protection and exception handling
        @type method_name: text_type
        @param method_name: vk api method name
        @param args: method parameters
        @param additional_timeout: time in seconds to wait before reattempting
        """
        assert isinstance(method_name, text_type)

        if retry > MAX_API_RETRY:
            raise IncorrectApiResponse('reached max api retry for %s, %s' % (method_name, self.jid))

        args = args or {}
        args.update({'v': self.VERSION, 'access_token': self.token})
        _logger.debug('calling api method %s, arguments: %s' % (method_name, args))

        time.sleep(additional_timeout)
        now = time.time()
        diff = now - self.last_method_time
        if diff < API_MAXIMUM_RATE:
            _logger.debug('burst protected')
            time.sleep(abs(diff - API_MAXIMUM_RATE))
        self.last_method_time = now

        try:
            response = requests.post(self.URL % method_name, args)
            if response.status_code != 200:
                raise requests.HTTPError('incorrect response status code')
            body = json.loads(response.text)
            _logger.debug('got: %s' % body)
            if 'response' in body:
                return body['response']
            if 'error' in body and 'error_code' in body['error']:
                code = body['error']['error_code']
                raise api_errors.get(code, UnknownError())
            raise NotImplementedError('unable to process %s' % body)
        except (requests.RequestException, ValueError) as e:
            _logger.error('method error: %s' % e)
            additional_timeout = additional_timeout or 1
        except TooManyRequestsPerSecond:
            additional_timeout = additional_timeout or API_MAXIMUM_RATE / WAIT_RATE
        additional_timeout *= WAIT_RATE
        return self._method(method_name, args, additional_timeout, retry + 1)
Пример #11
0
    def start(self):
        if self.polling:
            return _logger.debug('already polling %s' % self.user)
        self.polling = True
        args = self.user.vk.messages.get_lp_server()
        args['wait'] = 30
        url = 'http://{server}?act=a_check&key={key}&ts={ts}&wait={wait}&mode=2'.format(**args)
        data = json.loads(requests.get(url).text)
        try:
            if not data['updates']:
                _logger.debug('no updates for %s' % self.user)

            for update in data['updates']:
                gevent.spawn(self.handle, update)
        except KeyError:
            _logger.error('unable to process %s' % data)
        self.polling = False
        if not self.user.is_client:
            return _logger.debug('ending polling for %s' % self.user)
        self.start()
Пример #12
0
 def _render_fields(self, req, form_id, state):
     fields = json.loads(state is not None and state or '{}')
     rendered = []
     for name, value in fields.iteritems():
         if value == 'on':
             value = _("checked (checkbox)")
         elif value == '':
             value = _("empty (text field)")
         elif isinstance(value, str):
             value = "'".join(['', value, ''])
         else:
             # Still try to display something useful instead of corrupting
             #   parent page beyond hope of recovery through the web_ui.
             value = "'".join(['', repr(value), ''])
         author, time = self.get_tracform_fieldinfo(form_id, name)
         author = format_author(self.env, req, author, 'value')
         rendered.append(
             {'name': name, 'value': value,
              'author': tag.span(tag(_("by %(author)s", author=author)),
                                 class_='author'),
              'time': time})
     return rendered
Пример #13
0
def _render_values(state):
    fields = []
    for name, value in json.loads(state or '{}').iteritems():
        fields.append(name + ': ' + value)
    return '; '.join(fields)
Пример #14
0
                            except Exception, e:
                                errors.append(traceback.format_exc())
            else:
                if self.showErrors:
                    textlines.extend(errors)
                textlines.append(line)
                textlines.extend(srciter)

        # Determine our destination context and load the current state.
        self.context = tuple([realm, resource_id,
                              self.subcontext is not None and \
                              self.subcontext or ''])
        state = self.macro.get_tracform_state(self.context)
        self.formatter.env.log.debug('TracForms state = ' +
                                     (state is not None and state or ''))
        for name, value in json.loads(state or '{}').iteritems():
            self.env[name] = value
            self.formatter.env.log.debug(name + ' = ' + to_unicode(value))
            if self.subcontext is not None:
                self.env[self.subcontext + ':' + name] = value
        self.sorted_env = None
        (self.form_id, self.form_realm, self.form_resource_id,
            self.form_subcontext, self.form_updater, self.form_updated_on,
            self.form_keep_history, self.form_track_fields) = \
            self.macro.get_tracform_meta(self.context)
        self.form_id = self.form_id is not None and int(self.form_id) or None

        # Wiki-ize the text, this will allow other macros to execute after
        # which we can do our own replacements within whatever formatted
        # junk is left over.
        text = self.wiki('\n'.join(textlines))
Пример #15
0
 def addform(self, data):
     for name, value in json.loads(state or '{}').iteritems():
         keys = [prefix + ':' + name for prefix in self.prefixes]
         for key in keys:
             self[key] = tuple(value)
Пример #16
0
                            except Exception, e:
                                errors.append(traceback.format_exc())
            else:
                if self.showErrors:
                    textlines.extend(errors)
                textlines.append(line)
                textlines.extend(srciter)

        # Determine our destination context and load the current state.
        self.context = tuple([realm, resource_id,
                              self.subcontext is not None and \
                              self.subcontext or ''])
        state = self.macro.get_tracform_state(self.context)
        self.formatter.env.log.debug(
            'TracForms state = ' + (state is not None and state or ''))
        for name, value in json.loads(state or '{}').iteritems():
            self.env[name] = _xml_escape(value)
            self.formatter.env.log.debug(
                name + ' = ' + to_unicode(value))
            if self.subcontext is not None:
                self.env[self.subcontext + ':' + name] = value
        self.sorted_env = None
        (self.form_id, self.form_realm, self.form_resource_id,
            self.form_subcontext, self.form_updater, self.form_updated_on,
            self.form_keep_history, self.form_track_fields) = \
            self.macro.get_tracform_meta(self.context)
        self.form_id = self.form_id is not None and int(self.form_id) or None

        # Wiki-ize the text, this will allow other macros to execute after
        # which we can do our own replacements within whatever formatted
        # junk is left over.
Пример #17
0
    def save_tracform(self,
                      src,
                      state,
                      author,
                      base_version=None,
                      keep_history=False,
                      track_fields=False,
                      cursor=None):
        cursor = self.get_cursor(cursor)
        (form_id, realm, resource_id, subcontext, last_updater,
         last_updated_on, form_keep_history,
         form_track_fields) = self.get_tracform_meta(src)

        if form_keep_history is not None:
            keep_history = form_keep_history
        old_state = self.get_tracform_state(src)
        if form_track_fields is not None:
            track_fields = form_track_fields

        if base_version is not None:
            base_version = int(base_version or 0)

        if ((base_version is None and last_updated_on is None)
                or (base_version == last_updated_on)):
            if state != old_state:
                updated_on = int(time.time())
                if form_id is None:
                    form_id = cursor("""
                        INSERT INTO forms
                            (realm, resource_id, subcontext,
                            state, author, time)
                            VALUES (%s, %s, %s, %s, %s, %s)
                        """, realm, resource_id, subcontext,
                        state, author, updated_on) \
                        .last_id(cursor, 'forms', 'id')
                else:
                    cursor(
                        """
                        UPDATE  forms
                        SET     state = %s,
                                author = %s,
                                time = %s
                        WHERE   id = %s
                        """, state, author, updated_on, form_id)
                    if keep_history:
                        cursor(
                            """
                            INSERT INTO forms_history
                                    (id, time, author, old_state)
                                    VALUES (%s, %s, %s, %s)
                            """, form_id, last_updated_on, last_updater,
                            old_state)
                if track_fields:
                    # Break down old version and new version.
                    old_fields = json.loads(old_state or '{}')
                    new_fields = json.loads(state or '{}')
                    updated_fields = []
                    for field, old_value in old_fields.iteritems():
                        if new_fields.get(field) != old_value:
                            updated_fields.append(field)
                    for field in new_fields:
                        if old_fields.get(field) is None:
                            updated_fields.append(field)
                    self.log.debug('UPDATED: ' + str(updated_fields))
                    for field in updated_fields:
                        if cursor(
                                """
                            SELECT  COUNT(*)
                            FROM    forms_fields
                            WHERE   id = %s
                                AND field = %s""", form_id, field).value:

                            cursor(
                                """
                                UPDATE  forms_fields
                                    SET author = %s,
                                        time = %s
                                WHERE   id = %s
                                    AND field = %s
                                """, author, updated_on, form_id, field)
                        else:
                            cursor(
                                """
                                INSERT INTO forms_fields
                                        (id, field, author, time)
                                VALUES  (%s, %s, %s, %s)
                                """, form_id, field, author, updated_on)
            else:
                updated_on = last_updated_on
                author = last_updater
            return ((form_id, realm, resource_id, subcontext, state, author,
                     updated_on), (form_id, realm, resource_id, subcontext,
                                   old_state, last_updater, last_updated_on))
        else:
            raise ValueError(_("Conflict"))
Пример #18
0
    def save_tracform(self, src, state, author,
                        base_version=None, keep_history=False,
                        track_fields=False, db=None):
        (form_id, realm, resource_id, subcontext, last_updater,
            last_updated_on, form_keep_history,
            form_track_fields) = self.get_tracform_meta(src, db=db)

        if form_keep_history is not None:
            keep_history = form_keep_history
        old_state = form_id and self.get_tracform_state(form_id) or '{}'
        if form_track_fields is not None:
            track_fields = form_track_fields

        if base_version is not None:
            base_version = int(base_version or 0)

        if ((base_version is None and last_updated_on is None) or
            (base_version == last_updated_on)):
            if state != old_state:
                updated_on = int(time.time())
                db = self._get_db(db)
                cursor = db.cursor()
                if form_id is None:
                    cursor.execute("""
                        INSERT INTO forms
                            (realm, resource_id, subcontext,
                            state, author, time)
                            VALUES (%s, %s, %s, %s, %s, %s)
                        """, (realm, resource_id, subcontext,
                        state, author, updated_on))
                    form_id = db.get_last_id(cursor, 'forms') 
                else:
                    cursor.execute("""
                        UPDATE  forms
                        SET     state=%s,
                                author=%s,
                                time=%s
                        WHERE   id=%s
                        """, (state, author, updated_on, form_id))
                    if keep_history:
                        cursor.execute("""
                            INSERT INTO forms_history
                                    (id, time, author, old_state)
                                    VALUES (%s, %s, %s, %s)
                            """, (form_id, last_updated_on,
                                last_updater, old_state))
                if track_fields:
                    # Break down old version and new version.
                    old_fields = json.loads(old_state)
                    new_fields = json.loads(state or '{}')
                    updated_fields = []
                    for field, old_value in old_fields.iteritems():
                        if new_fields.get(field) != old_value:
                            updated_fields.append(field)
                    for field in new_fields:
                        if old_fields.get(field) is None:
                            updated_fields.append(field)
                    self.log.debug('UPDATED: ' + str(updated_fields))
                    for field in updated_fields:
                        cursor.execute("""
                            SELECT  COUNT(*)
                            FROM    forms_fields
                            WHERE   id=%s
                                AND field=%s""", (form_id, field))
                        if cursor.fetchone()[0] > 0:
                            cursor.execute("""
                                UPDATE  forms_fields
                                    SET author=%s,
                                        time=%s
                                WHERE   id=%s
                                    AND field=%s
                                """, (author, updated_on, form_id, field))
                        else:
                            cursor.execute("""
                                INSERT INTO forms_fields
                                        (id, field, author, time)
                                VALUES  (%s, %s, %s, %s)
                                """, (form_id, field, author, updated_on))
                db.commit()
            else:
                updated_on = last_updated_on
                author = last_updater
            return ((form_id, realm, resource_id, subcontext, state,
                    author, updated_on),
                    (form_id, realm, resource_id, subcontext, old_state,
                    last_updater, last_updated_on))
        else:
            raise ValueError(_("Conflict"))
Пример #19
0
 def addform(self, data):
     for name, value in json.loads(state or '{}').iteritems():
         keys = [prefix + ':' + name for prefix in self.prefixes]
         for key in keys:
             self[key] = tuple(value)
Пример #20
0
def parse_history(changes, fieldwise=False):
    """Versatile history parser for TracForms.

    Returns either a list of dicts for changeset display in form view or
    a dict of field change lists for stepwise form reset.
    """
    fieldhistory = {}
    history = []
    if not fieldwise == False:
        def _add_change(fieldhistory, field, author, time, old, new):
            if field not in fieldhistory.keys():
                fieldhistory[field] = [{'author': author, 'time': time,
                                        'old': old, 'new': new}]
            else:
                fieldhistory[field].append({'author': author, 'time': time,
                                            'old': old, 'new': new})
            return fieldhistory

    new_fields = None
    for changeset in changes:
        # break down old and new version
        try:
            old_fields = json.loads(changeset.get('old_state', '{}'))
        except ValueError:
            # skip invalid history
            old_fields = {}
            pass
        if new_fields is None:
            # first loop cycle: only load values for comparison next time
            new_fields = old_fields
            last_author = changeset['author']
            last_change = changeset['time']
            continue
        updated_fields = {}
        for field, old_value in old_fields.iteritems():
            new_value = new_fields.get(field)
            if new_value != old_value:
                if fieldwise == False:
                    change = _render_change(old_value, new_value)
                    if change is not None:
                        updated_fields[field] = change
                else:
                    fieldhistory = _add_change(fieldhistory, field,
                                               last_author, last_change,
                                               old_value, new_value)
        for field in new_fields:
            if old_fields.get(field) is None:
                if fieldwise == False:
                    change = _render_change(None, new_fields[field])
                    if change is not None:
                        updated_fields[field] = change
                else:
                    fieldhistory = _add_change(fieldhistory, field,
                                               last_author, last_change,
                                               None, new_fields[field])
        new_fields = old_fields
        history.append({'author': last_author,
                        'time': format_datetime(last_change),
                        'changes': updated_fields})
        last_author = changeset['author']
        last_change = changeset['time']
    return fieldwise == False and history or fieldhistory
Пример #21
0
def _render_values(state, delimiter=': '):
    fields = []
    for name, value in json.loads(state or '{}').iteritems():
        fields.append(''.join([name, delimiter, value]))
    return '; '.join(fields)