<commit_before>#!/usr/bin/env python
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Download files from Google Storage based on SHA1 sums."""

import hashlib
import optparse
import os
import Queue
import re
import sys
import threading
import time

import subprocess2

GSUTIL_DEFAULT_PATH = os.path.join(
    os.path.dirname(os.path.abspath(__file__)),
    'third_party', 'gsutil', 'gsutil')

class FileNotFoundError(IOError):
  pass

class InvalidFileError(IOError):
  pass

# Common utilities
class Gsutil(object):
  """Call gsutil with some predefined settings.  This is a convenience object,
  and is also immutable."""
  def __init__(self, path, boto_path, timeout=None):
    if not os.path.exists(path):
      raise FileNotFoundError('GSUtil not found in %s' % path)
    self.path = path
    self.timeout = timeout
    self.boto_path = boto_path

  def get_sub_env(self):
    env = os.environ.copy()
    if self.boto_path:
      env['AWS_CREDENTIAL_FILE'] = self.boto_path
    else:
      custompath = env.get('AWS_CREDENTIAL_FILE', '~/.boto') + '.depot_tools'
      custompath = os.path.expanduser(custompath)
      if os.path.exists(custompath):
        env['AWS_CREDENTIAL_FILE'] = custompath

    return env

  def call(self, *args):
    return subprocess2.call((sys.executable, self.path) + args,
                            env=self.get_sub_env(),
                            timeout=self.timeout)

  def check_call(self, *args):
    ((out, err), code) = subprocess2.communicate(
        (sys.executable, self.path) + args,
        stdout=subprocess2.PIPE,
        stderr=subprocess2.PIPE,
        env=self.get_sub_env(),
        timeout=self.timeout)

    # Parse output.
    status_code_match = re.search('status=([0-9]+)', err)
    if status_code_match:
      return (int(status_code_match.group(1)), out, err)
    if ('You are attempting to access protected data with '
          'no configured credentials.' in err):
      return (403, out, err)
    if 'No such object' in err:
      return (404, out, err)
    return (code, out, err)

def check_bucket_permissions(bucket, gsutil):
  if not bucket:
    print >> sys.stderr, 'Missing bucket %s.'
    return (None, 1)
  base_url = 'gs://%s' % bucket

  code, _, ls_err = gsutil.check_call('ls', base_url)
  if code == 403:
    print >> sys.stderr, 'Got error 403 while authenticating to %s.' % base_url
    print >> sys.stderr, 'Try running "download_from_google_storage --config".'
  elif code == 404:
    print >> sys.stderr, '%s not found.' % base_url
  elif code != 0:
    print >> sys.stderr, ls_err
  return (base_url, code)

def get_sha1(filename):
  sha1 = hashlib.sha1()
  with open(filename, 'rb') as f:
    while True:
      # Read in 1mb chunks, so it doesn't all have to be loaded into memory.
      chunk = f.read(1024*1024)
      if not chunk:
        break
      sha1.update(chunk)
  return sha1.hexdigest()

# Download-specific code starts here

def enumerate_work_queue(input_filename, work_queue, directory,
                         recursive, ignore_errors, output, sha1_file):
  if sha1_file:
    if not os.path.exists(input_filename):
      if not ignore_errors:
        raise FileNotFoundError('%s not found.' % input_filename)
      print >> sys.stderr, '%s not found.' % input_filename
    with open(input_filename, 'rb') as f:
      sha1_match = re.match('^([A-Za-z0-9]{40})$', f.read(1024).rstrip())
      if sha1_match:
        work_queue.put(
            (sha1_match.groups(1)[0], input_filename.replace('.sha1', '')))
        return 1
    if not ignore_errors:
      raise InvalidFileError('No sha1 sum found in %s.' % input_filename)
    print >> sys.stderr, 'No sha1 sum found in %s.' % input_filename
    return 0

  if not directory:
    work_queue.put((input_filename, output))
    return 1

  work_queue_size = 0
  for root, dirs, files in os.walk(input_filename):
    if not recursive:
      for item in dirs[:]:
        dirs.remove(item)
    else:
      for exclude in ['.svn', '.git']:
        if exclude in dirs:
          dirs.remove(exclude)
    for filename in files:
      full_path = os.path.join(root, filename)
      if full_path.endswith('.sha1'):
        with open(full_path, 'rb') as f:
          sha1_match = re.match('^([A-Za-z0-9]{40})$', f.read(1024).rstrip())
        if sha1_match:
          work_queue.put(
              (sha1_match.groups(1)[0], full_path.replace('.sha1', '')))
          work_queue_size += 1
        else:
          if not ignore_errors:
            raise InvalidFileError('No sha1 sum found in %s.' % filename)
          print >> sys.stderr, 'No sha1 sum found in %s.' % filename
  return work_queue_size

def _downloader_worker_thread(thread_num, q, force, base_url,
                              gsutil, out_q, ret_codes):
  while True:
    input_sha1_sum, output_filename = q.get()
    if input_sha1_sum is None:
      return
    if os.path.exists(output_filename) and not force:
      if get_sha1(output_filename) == input_sha1_sum:
        out_q.put(
            '%d> File %s exists and SHA1 matches. Skipping.' % (
                thread_num, output_filename))
        continue
    # Check if file exists.
    file_url = '%s/%s' % (base_url, input_sha1_sum)
    if gsutil.check_call('ls', file_url)[0] != 0:
      out_q.put('%d> File %s for %s does not exist, skipping.' % (
          thread_num, file_url, output_filename))
      ret_codes.put((1, 'File %s for %s does not exist.' % (
          file_url, output_filename)))
      continue
    # Fetch the file.
    out_q.put('%d> Downloading %s...' % (
        thread_num, output_filename))
    code, _, err = gsutil.check_call('cp', '-q', file_url, output_filename)
    if code != 0:
      out_q.put('%d> %s' % (thread_num, err))
      ret_codes.put((code, err))

def printer_worker(output_queue):
  while True:
    line = output_queue.get()
    # Its plausible we want to print empty lines.
    if line is None:
      break
    print line

def download_from_google_storage(
    input_filename, base_url, gsutil, num_threads, directory, recursive,
    force, output, ignore_errors, sha1_file):
  # Start up all the worker threads.
  all_threads = []
  download_start = time.time()
  stdout_queue = Queue.Queue()
  work_queue = Queue.Queue()
  ret_codes = Queue.Queue()
  ret_codes.put((0, None))
  for thread_num in range(num_threads):
    t = threading.Thread(
        target=_downloader_worker_thread,
        args=[thread_num, work_queue, force, base_url,
              gsutil, stdout_queue, ret_codes])
    t.daemon = True
    t.start()
    all_threads.append(t)
  printer_thread = threading.Thread(target=printer_worker, args=[stdout_queue])
  printer_thread.daemon = True
  printer_thread.start()

  # Enumerate our work queue.
  work_queue_size = enumerate_work_queue(
      input_filename, work_queue, directory, recursive,
      ignore_errors, output, sha1_file)
  for _ in all_threads:
    work_queue.put((None, None))  # Used to tell worker threads to stop.

  # Wait for all downloads to finish.
  for t in all_threads:
    t.join()
  stdout_queue.put(None)
  printer_thread.join()

  # See if we ran into any errors.
  max_ret_code = 0
  for ret_code, message in ret_codes.queue:
    max_ret_code = max(ret_code, max_ret_code)
    if message:
      print >> sys.stderr, message
  if not max_ret_code:
    print 'Success!'

  print 'Downloading %d files took %1f second(s)' % (
      work_queue_size, time.time() - download_start)
  return max_ret_code

def main(args):
  usage = ('usage: %prog [options] target\n'
           'Target must be:\n'
           '  (default) a sha1 sum ([A-Za-z0-9]{40}).\n'
           '  (-s or --sha1_file) a .sha1 file, containing a sha1 sum on '
           'the first line.\n'
           '  (-d or --directory) A directory to scan for .sha1 files.')
  parser = optparse.OptionParser(usage)
  parser.add_option('-o', '--output',
                    help='Specify the output file name. Defaults to: '
                         '(a) Given a SHA1 hash, the name is the SHA1 hash. '
                         '(b) Given a .sha1 file or directory, the name will '
                         'match (.*).sha1.')
  parser.add_option('-b', '--bucket',
                    help='Google Storage bucket to fetch from.')
  parser.add_option('-e', '--boto',
                    help='Specify a custom boto file.')
  parser.add_option('-c', '--no_resume', action='store_true',
                    help='Resume download if file is partially downloaded.')
  parser.add_option('-f', '--force', action='store_true',
                    help='Force download even if local file exists.')
  parser.add_option('-i', '--ignore_errors', action='store_true',
                    help='Don\'t throw error if we find an invalid .sha1 file.')
  parser.add_option('-r', '--recursive', action='store_true',
                    help='Scan folders recursively for .sha1 files. '
                         'Must be used with -d/--directory')
  parser.add_option('-t', '--num_threads', default=1, type='int',
                    help='Number of downloader threads to run.')
  parser.add_option('-d', '--directory', action='store_true',
                    help='The target is a directory.  '
                         'Cannot be used with -s/--sha1_file.')
  parser.add_option('-s', '--sha1_file', action='store_true',
                    help='The target is a file containing a sha1 sum.  '
                         'Cannot be used with -d/--directory.')
  parser.add_option('-g', '--config', action='store_true',
                    help='Alias for "gsutil config".  Run this if you want '
                         'to initialize your saved Google Storage '
                         'credentials.')

  (options, args) = parser.parse_args()
  # First, make sure we can find a working instance of gsutil.
  if os.path.exists(GSUTIL_DEFAULT_PATH):
    gsutil = Gsutil(GSUTIL_DEFAULT_PATH, boto_path=options.boto)
  else:
    gsutil = None
    for path in os.environ["PATH"].split(os.pathsep):
      if os.path.exists(path) and 'gsutil' in os.listdir(path):
        gsutil = Gsutil(os.path.join(path, 'gsutil'), boto_path=options.boto)
    if not gsutil:
      parser.error('gsutil not found in %s, bad depot_tools checkout?' %
                  GSUTIL_DEFAULT_PATH)

  # Passing in -g/--config will run our copy of GSUtil, then quit.
  if options.config:
    return gsutil.call('config')

  if not args:
    parser.error('Missing target.')
  if len(args) > 1:
    parser.error('Too many targets.')
  if not options.bucket:
    parser.error('Missing bucket.  Specify bucket with --bucket.')
  if options.sha1_file and options.directory:
    parser.error('Both --directory and --sha1_file are specified, '
                 'can only specify one.')
  if options.recursive and not options.directory:
    parser.error('--recursive specified but --directory not specified.')
  if options.output and options.directory:
    parser.error('--directory is specified, so --output has no effect.')
  input_filename = args[0]

  # Set output filename if not specified.
  if not options.output and not options.directory:
    if not options.sha1_file:
      # Target is a sha1 sum, so output filename would also be the sha1 sum.
      options.output = input_filename
    elif options.sha1_file:
      # Target is a .sha1 file.
      if not input_filename.endswith('.sha1'):
        parser.error('--sha1_file is specified, but the input filename '
                     'does not end with .sha1, and no --output is specified. '
                     'Either make sure the input filename has a .sha1 '
                     'extension, or specify --output.')
      options.output = input_filename[:-5]
    else:
      parser.error('Unreachable state.')

  # Check if output file already exists.
  if not options.directory and not options.force and not options.no_resume:
    if os.path.exists(options.output):
      parser.error('Output file %s exists and --no_resume is specified.'
                   % options.output)

  # Check we have a valid bucket with valid permissions.
  base_url, code = check_bucket_permissions(options.bucket, gsutil)
  if code:
    return code

  return download_from_google_storage(
      input_filename, base_url, gsutil, options.num_threads, options.directory,
      options.recursive, options.force, options.output, options.ignore_errors,
      options.sha1_file)

if __name__ == '__main__':
  sys.exit(main(sys.argv))
<commit_msg>Print gsutil error message when we enconter as 403 or 404 We appear to be getting 403's when encountering errors other than mismatched credentials.  The error message was originally hidden to avoid clutter, but it has become obvious that there are other transient errors hidden behind a 403.<commit_after>#!/usr/bin/env python
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Download files from Google Storage based on SHA1 sums."""

import hashlib
import optparse
import os
import Queue
import re
import sys
import threading
import time

import subprocess2

GSUTIL_DEFAULT_PATH = os.path.join(
    os.path.dirname(os.path.abspath(__file__)),
    'third_party', 'gsutil', 'gsutil')

class FileNotFoundError(IOError):
  pass

class InvalidFileError(IOError):
  pass

# Common utilities
class Gsutil(object):
  """Call gsutil with some predefined settings.  This is a convenience object,
  and is also immutable."""
  def __init__(self, path, boto_path, timeout=None):
    if not os.path.exists(path):
      raise FileNotFoundError('GSUtil not found in %s' % path)
    self.path = path
    self.timeout = timeout
    self.boto_path = boto_path

  def get_sub_env(self):
    env = os.environ.copy()
    if self.boto_path:
      env['AWS_CREDENTIAL_FILE'] = self.boto_path
    else:
      custompath = env.get('AWS_CREDENTIAL_FILE', '~/.boto') + '.depot_tools'
      custompath = os.path.expanduser(custompath)
      if os.path.exists(custompath):
        env['AWS_CREDENTIAL_FILE'] = custompath

    return env

  def call(self, *args):
    return subprocess2.call((sys.executable, self.path) + args,
                            env=self.get_sub_env(),
                            timeout=self.timeout)

  def check_call(self, *args):
    ((out, err), code) = subprocess2.communicate(
        (sys.executable, self.path) + args,
        stdout=subprocess2.PIPE,
        stderr=subprocess2.PIPE,
        env=self.get_sub_env(),
        timeout=self.timeout)

    # Parse output.
    status_code_match = re.search('status=([0-9]+)', err)
    if status_code_match:
      return (int(status_code_match.group(1)), out, err)
    if ('You are attempting to access protected data with '
          'no configured credentials.' in err):
      return (403, out, err)
    if 'No such object' in err:
      return (404, out, err)
    return (code, out, err)

def check_bucket_permissions(bucket, gsutil):
  if not bucket:
    print >> sys.stderr, 'Missing bucket %s.'
    return (None, 1)
  base_url = 'gs://%s' % bucket

  code, _, ls_err = gsutil.check_call('ls', base_url)
  if code != 0:
    print >> sys.stderr, ls_err
  if code == 403:
    print >> sys.stderr, 'Got error 403 while authenticating to %s.' % base_url
    print >> sys.stderr, 'Try running "download_from_google_storage --config".'
  elif code == 404:
    print >> sys.stderr, '%s not found.' % base_url
  return (base_url, code)

def get_sha1(filename):
  sha1 = hashlib.sha1()
  with open(filename, 'rb') as f:
    while True:
      # Read in 1mb chunks, so it doesn't all have to be loaded into memory.
      chunk = f.read(1024*1024)
      if not chunk:
        break
      sha1.update(chunk)
  return sha1.hexdigest()

# Download-specific code starts here

def enumerate_work_queue(input_filename, work_queue, directory,
                         recursive, ignore_errors, output, sha1_file):
  if sha1_file:
    if not os.path.exists(input_filename):
      if not ignore_errors:
        raise FileNotFoundError('%s not found.' % input_filename)
      print >> sys.stderr, '%s not found.' % input_filename
    with open(input_filename, 'rb') as f:
      sha1_match = re.match('^([A-Za-z0-9]{40})$', f.read(1024).rstrip())
      if sha1_match:
        work_queue.put(
            (sha1_match.groups(1)[0], input_filename.replace('.sha1', '')))
        return 1
    if not ignore_errors:
      raise InvalidFileError('No sha1 sum found in %s.' % input_filename)
    print >> sys.stderr, 'No sha1 sum found in %s.' % input_filename
    return 0

  if not directory:
    work_queue.put((input_filename, output))
    return 1

  work_queue_size = 0
  for root, dirs, files in os.walk(input_filename):
    if not recursive:
      for item in dirs[:]:
        dirs.remove(item)
    else:
      for exclude in ['.svn', '.git']:
        if exclude in dirs:
          dirs.remove(exclude)
    for filename in files:
      full_path = os.path.join(root, filename)
      if full_path.endswith('.sha1'):
        with open(full_path, 'rb') as f:
          sha1_match = re.match('^([A-Za-z0-9]{40})$', f.read(1024).rstrip())
        if sha1_match:
          work_queue.put(
              (sha1_match.groups(1)[0], full_path.replace('.sha1', '')))
          work_queue_size += 1
        else:
          if not ignore_errors:
            raise InvalidFileError('No sha1 sum found in %s.' % filename)
          print >> sys.stderr, 'No sha1 sum found in %s.' % filename
  return work_queue_size

def _downloader_worker_thread(thread_num, q, force, base_url,
                              gsutil, out_q, ret_codes):
  while True:
    input_sha1_sum, output_filename = q.get()
    if input_sha1_sum is None:
      return
    if os.path.exists(output_filename) and not force:
      if get_sha1(output_filename) == input_sha1_sum:
        out_q.put(
            '%d> File %s exists and SHA1 matches. Skipping.' % (
                thread_num, output_filename))
        continue
    # Check if file exists.
    file_url = '%s/%s' % (base_url, input_sha1_sum)
    if gsutil.check_call('ls', file_url)[0] != 0:
      out_q.put('%d> File %s for %s does not exist, skipping.' % (
          thread_num, file_url, output_filename))
      ret_codes.put((1, 'File %s for %s does not exist.' % (
          file_url, output_filename)))
      continue
    # Fetch the file.
    out_q.put('%d> Downloading %s...' % (
        thread_num, output_filename))
    code, _, err = gsutil.check_call('cp', '-q', file_url, output_filename)
    if code != 0:
      out_q.put('%d> %s' % (thread_num, err))
      ret_codes.put((code, err))

def printer_worker(output_queue):
  while True:
    line = output_queue.get()
    # Its plausible we want to print empty lines.
    if line is None:
      break
    print line

def download_from_google_storage(
    input_filename, base_url, gsutil, num_threads, directory, recursive,
    force, output, ignore_errors, sha1_file):
  # Start up all the worker threads.
  all_threads = []
  download_start = time.time()
  stdout_queue = Queue.Queue()
  work_queue = Queue.Queue()
  ret_codes = Queue.Queue()
  ret_codes.put((0, None))
  for thread_num in range(num_threads):
    t = threading.Thread(
        target=_downloader_worker_thread,
        args=[thread_num, work_queue, force, base_url,
              gsutil, stdout_queue, ret_codes])
    t.daemon = True
    t.start()
    all_threads.append(t)
  printer_thread = threading.Thread(target=printer_worker, args=[stdout_queue])
  printer_thread.daemon = True
  printer_thread.start()

  # Enumerate our work queue.
  work_queue_size = enumerate_work_queue(
      input_filename, work_queue, directory, recursive,
      ignore_errors, output, sha1_file)
  for _ in all_threads:
    work_queue.put((None, None))  # Used to tell worker threads to stop.

  # Wait for all downloads to finish.
  for t in all_threads:
    t.join()
  stdout_queue.put(None)
  printer_thread.join()

  # See if we ran into any errors.
  max_ret_code = 0
  for ret_code, message in ret_codes.queue:
    max_ret_code = max(ret_code, max_ret_code)
    if message:
      print >> sys.stderr, message
  if not max_ret_code:
    print 'Success!'

  print 'Downloading %d files took %1f second(s)' % (
      work_queue_size, time.time() - download_start)
  return max_ret_code

def main(args):
  usage = ('usage: %prog [options] target\n'
           'Target must be:\n'
           '  (default) a sha1 sum ([A-Za-z0-9]{40}).\n'
           '  (-s or --sha1_file) a .sha1 file, containing a sha1 sum on '
           'the first line.\n'
           '  (-d or --directory) A directory to scan for .sha1 files.')
  parser = optparse.OptionParser(usage)
  parser.add_option('-o', '--output',
                    help='Specify the output file name. Defaults to: '
                         '(a) Given a SHA1 hash, the name is the SHA1 hash. '
                         '(b) Given a .sha1 file or directory, the name will '
                         'match (.*).sha1.')
  parser.add_option('-b', '--bucket',
                    help='Google Storage bucket to fetch from.')
  parser.add_option('-e', '--boto',
                    help='Specify a custom boto file.')
  parser.add_option('-c', '--no_resume', action='store_true',
                    help='Resume download if file is partially downloaded.')
  parser.add_option('-f', '--force', action='store_true',
                    help='Force download even if local file exists.')
  parser.add_option('-i', '--ignore_errors', action='store_true',
                    help='Don\'t throw error if we find an invalid .sha1 file.')
  parser.add_option('-r', '--recursive', action='store_true',
                    help='Scan folders recursively for .sha1 files. '
                         'Must be used with -d/--directory')
  parser.add_option('-t', '--num_threads', default=1, type='int',
                    help='Number of downloader threads to run.')
  parser.add_option('-d', '--directory', action='store_true',
                    help='The target is a directory.  '
                         'Cannot be used with -s/--sha1_file.')
  parser.add_option('-s', '--sha1_file', action='store_true',
                    help='The target is a file containing a sha1 sum.  '
                         'Cannot be used with -d/--directory.')
  parser.add_option('-g', '--config', action='store_true',
                    help='Alias for "gsutil config".  Run this if you want '
                         'to initialize your saved Google Storage '
                         'credentials.')

  (options, args) = parser.parse_args()
  # First, make sure we can find a working instance of gsutil.
  if os.path.exists(GSUTIL_DEFAULT_PATH):
    gsutil = Gsutil(GSUTIL_DEFAULT_PATH, boto_path=options.boto)
  else:
    gsutil = None
    for path in os.environ["PATH"].split(os.pathsep):
      if os.path.exists(path) and 'gsutil' in os.listdir(path):
        gsutil = Gsutil(os.path.join(path, 'gsutil'), boto_path=options.boto)
    if not gsutil:
      parser.error('gsutil not found in %s, bad depot_tools checkout?' %
                  GSUTIL_DEFAULT_PATH)

  # Passing in -g/--config will run our copy of GSUtil, then quit.
  if options.config:
    return gsutil.call('config')

  if not args:
    parser.error('Missing target.')
  if len(args) > 1:
    parser.error('Too many targets.')
  if not options.bucket:
    parser.error('Missing bucket.  Specify bucket with --bucket.')
  if options.sha1_file and options.directory:
    parser.error('Both --directory and --sha1_file are specified, '
                 'can only specify one.')
  if options.recursive and not options.directory:
    parser.error('--recursive specified but --directory not specified.')
  if options.output and options.directory:
    parser.error('--directory is specified, so --output has no effect.')
  input_filename = args[0]

  # Set output filename if not specified.
  if not options.output and not options.directory:
    if not options.sha1_file:
      # Target is a sha1 sum, so output filename would also be the sha1 sum.
      options.output = input_filename
    elif options.sha1_file:
      # Target is a .sha1 file.
      if not input_filename.endswith('.sha1'):
        parser.error('--sha1_file is specified, but the input filename '
                     'does not end with .sha1, and no --output is specified. '
                     'Either make sure the input filename has a .sha1 '
                     'extension, or specify --output.')
      options.output = input_filename[:-5]
    else:
      parser.error('Unreachable state.')

  # Check if output file already exists.
  if not options.directory and not options.force and not options.no_resume:
    if os.path.exists(options.output):
      parser.error('Output file %s exists and --no_resume is specified.'
                   % options.output)

  # Check we have a valid bucket with valid permissions.
  base_url, code = check_bucket_permissions(options.bucket, gsutil)
  if code:
    return code

  return download_from_google_storage(
      input_filename, base_url, gsutil, options.num_threads, options.directory,
      options.recursive, options.force, options.output, options.ignore_errors,
      options.sha1_file)

if __name__ == '__main__':
  sys.exit(main(sys.argv))
<|endoftext|>


<commit_before>from __future__ import unicode_literals

from django.contrib import auth
from django.core.exceptions import ValidationError
from django.db import IntegrityError
from rest_framework import serializers
from rest_framework.exceptions import APIException
from rest_framework.reverse import reverse

from nodeconductor.core import serializers as core_serializers
from nodeconductor.structure import models
from nodeconductor.structure.filters import filter_queryset_for_user

User = auth.get_user_model()

class BasicUserSerializer(serializers.HyperlinkedModelSerializer):
    class Meta(object):
        model = User
        fields = ('url', 'uuid', 'username', 'full_name', 'native_name',)
        lookup_field = 'uuid'

class BasicProjectSerializer(core_serializers.BasicInfoSerializer):
    class Meta(core_serializers.BasicInfoSerializer.Meta):
        model = models.Project

class BasicProjectGroupSerializer(core_serializers.BasicInfoSerializer):
    class Meta(core_serializers.BasicInfoSerializer.Meta):
        model = models.ProjectGroup

class ResourceQuotaSerializer(serializers.ModelSerializer):
    class Meta(object):
        model = models.ResourceQuota
        fields = ('vcpu', 'ram', 'storage', 'max_instances')

class ProjectSerializer(core_serializers.CollectedFieldsMixin,
                        core_serializers.RelatedResourcesFieldMixin,
                        serializers.HyperlinkedModelSerializer):
    project_groups = BasicProjectGroupSerializer(many=True, read_only=True)
    resource_quota = ResourceQuotaSerializer(read_only=True)
    resource_quota_usage = ResourceQuotaSerializer(read_only=True)

    class Meta(object):
        model = models.Project
        fields = ('url', 'uuid', 'name', 'customer', 'customer_name', 'project_groups', 'resource_quota',
                  'resource_quota_usage')
        lookup_field = 'uuid'

    def get_related_paths(self):
        return 'customer',

class ProjectCreateSerializer(core_serializers.PermissionFieldFilteringMixin,
                              serializers.HyperlinkedModelSerializer):

    resource_quota = ResourceQuotaSerializer(required=False)

    class Meta(object):
        model = models.Project
        fields = ('url', 'name', 'customer', 'resource_quota', 'project_groups')
        lookup_field = 'uuid'

    def get_filtered_field_names(self):
        return 'customer',

    def validate(self, attrs):
        try:
            user = self.context['request'].user
        except (KeyError, AttributeError):
            return attrs

        if user.is_staff:
            return attrs
        is_customer_owner = attrs['customer'].roles.filter(
            permission_group__user=user, role_type=models.CustomerRole.OWNER).exists()
        if is_customer_owner:
            return attrs

        groups = attrs['project_groups']
        if not groups:
            raise ValidationError('Project has to belong to some group')

        for group in groups:
            is_group_manager = group.roles.filter(
                permission_group__user=user, role_type=models.ProjectGroupRole.MANAGER).exists()
            if not is_group_manager:
                raise ValidationError('User does not have permission to add project to %s' % group)

        return attrs

class CustomerSerializer(core_serializers.CollectedFieldsMixin,
                         serializers.HyperlinkedModelSerializer):
    projects = serializers.SerializerMethodField('get_customer_projects')
    project_groups = serializers.SerializerMethodField('get_customer_project_groups')
    owners = BasicUserSerializer(source='get_owners', many=True, read_only=True)

    class Meta(object):
        model = models.Customer
        fields = ('url', 'uuid', 'name', 'abbreviation', 'contact_details', 'projects', 'project_groups', 'owners')
        lookup_field = 'uuid'

    def _get_filtered_data(self, objects, serializer):
        try:
            user = self.context['request'].user
        except (KeyError, AttributeError):
            return None

        queryset = filter_queryset_for_user(objects, user)
        serializer_instance = serializer(queryset, context={'request': self.context['request']})
        return serializer_instance.data

    def get_customer_projects(self, obj):
        return self._get_filtered_data(obj.projects.all(), BasicProjectSerializer)

    def get_customer_project_groups(self, obj):
        return self._get_filtered_data(obj.project_groups.all(), BasicProjectGroupSerializer)

class ProjectGroupSerializer(core_serializers.PermissionFieldFilteringMixin,
                             core_serializers.RelatedResourcesFieldMixin,
                             serializers.HyperlinkedModelSerializer):
    projects = BasicProjectSerializer(many=True, read_only=True)

    class Meta(object):
        model = models.ProjectGroup
        fields = ('url', 'uuid', 'name', 'customer', 'customer_name', 'projects')
        lookup_field = 'uuid'

    def get_filtered_field_names(self):
        return 'customer',

    def get_fields(self):
        # TODO: Extract to a proper mixin
        fields = super(ProjectGroupSerializer, self).get_fields()

        try:
            method = self.context['view'].request.method
        except (KeyError, AttributeError):
            return fields

        if method in ('PUT', 'PATCH'):
            fields['customer'].read_only = True

        return fields

    def get_related_paths(self):
        return 'customer',

class ProjectGroupMembershipSerializer(core_serializers.PermissionFieldFilteringMixin,
                                       serializers.HyperlinkedModelSerializer):
    project_group = serializers.HyperlinkedRelatedField(
        source='projectgroup',
        view_name='projectgroup-detail',
        lookup_field='uuid',
    )
    project_group_name = serializers.Field(source='projectgroup.name')
    project = serializers.HyperlinkedRelatedField(
        view_name='project-detail',
        lookup_field='uuid',
    )
    project_name = serializers.Field(source='project.name')

    class Meta(object):
        model = models.ProjectGroup.projects.through
        fields = (
            'url',
            'project_group', 'project_group_name',
            'project', 'project_name',
        )
        view_name = 'projectgroup_membership-detail'

    def get_filtered_field_names(self):
        return 'project', 'project_group'

class ProjectRoleField(serializers.ChoiceField):

    def field_to_native(self, obj, field_name):
        if obj is not None:
            return models.ProjectRole.ROLE_TO_NAME[obj.group.projectrole.role_type]

    def field_from_native(self, data, files, field_name, into):
        role = data.get('role')
        if role in models.ProjectRole.NAME_TO_ROLE:
            into[field_name] = models.ProjectRole.NAME_TO_ROLE[role]
        else:
            raise ValidationError('Unknown role')

class ProjectGroupRoleField(serializers.ChoiceField):

    def field_to_native(self, obj, field_name):
        if obj is not None:
            return models.ProjectGroupRole.ROLE_TO_NAME[obj.group.projectgrouprole.role_type]

    def field_from_native(self, data, files, field_name, into):
        role = data.get('role')
        if role in models.ProjectGroupRole.NAME_TO_ROLE:
            into[field_name] = models.ProjectGroupRole.NAME_TO_ROLE[role]
        else:
            raise ValidationError('Unknown role')

class CustomerRoleField(serializers.ChoiceField):

    def field_to_native(self, obj, field_name):
        if obj is not None:
            return models.CustomerRole.ROLE_TO_NAME[obj.group.customerrole.role_type]

    def field_from_native(self, data, files, field_name, into):
        role = data.get('role')
        if role in models.CustomerRole.NAME_TO_ROLE:
            into[field_name] = models.CustomerRole.NAME_TO_ROLE[role]
        else:
            raise ValidationError('Unknown role')

class ProjectPermissionReadSerializer(core_serializers.RelatedResourcesFieldMixin,
                                      serializers.HyperlinkedModelSerializer):
    user = serializers.HyperlinkedRelatedField(
        view_name='user-detail',
        lookup_field='uuid',
        queryset=User.objects.all(),
    )
    user_full_name = serializers.Field(source='user.full_name')
    user_native_name = serializers.Field(source='user.native_name')

    role = ProjectRoleField(choices=models.ProjectRole.TYPE_CHOICES)

    class Meta(object):
        model = User.groups.through
        fields = (
            'url',
            'project', 'project_name',
            'user', 'user_full_name', 'user_native_name',
            'role',
        )
        view_name = 'project_permission-detail'

    def get_related_paths(self):
        return 'group.projectrole.project',

class NotModifiedPermission(APIException):
    status_code = 304
    default_detail = 'Permissions were not modified'

# TODO: refactor to abstract class, subclass by CustomerPermissions and ProjectPermissions
class CustomerPermissionSerializer(core_serializers.PermissionFieldFilteringMixin,
                                   serializers.HyperlinkedModelSerializer):
    customer = serializers.HyperlinkedRelatedField(
        source='group.customerrole.customer',
        view_name='customer-detail',
        lookup_field='uuid',
        queryset=models.Customer.objects.all(),
    )
    customer_name = serializers.Field(source='group.customerrole.customer.name')

    user = serializers.HyperlinkedRelatedField(
        view_name='user-detail',
        lookup_field='uuid',
        queryset=User.objects.all(),
    )
    user_full_name = serializers.Field(source='user.full_name')
    user_native_name = serializers.Field(source='user.native_name')
    user_username = serializers.Field(source='user.username')

    role = CustomerRoleField(choices=models.CustomerRole.TYPE_CHOICES)

    class Meta(object):
        model = User.groups.through
        fields = (
            'url', 'role',
            'customer', 'customer_name',
            'user', 'user_username', 'user_full_name', 'user_native_name',
        )
        view_name = 'customer_permission-detail'

    def restore_object(self, attrs, instance=None):
        customer = attrs['group.customerrole.customer']
        group = customer.roles.get(role_type=attrs['role']).permission_group
        UserGroup = User.groups.through
        return UserGroup(user=attrs['user'], group=group)

    def save_object(self, obj, **kwargs):
        try:
            obj.save()
        except IntegrityError:
            raise NotModifiedPermission()

    def get_filtered_field_names(self):
        return 'customer',

class ProjectPermissionSerializer(core_serializers.PermissionFieldFilteringMixin,
                                  serializers.HyperlinkedModelSerializer):
    project = serializers.HyperlinkedRelatedField(source='group.projectrole.project', view_name='project-detail',
                                                  lookup_field='uuid', queryset=models.Project.objects.all())
    user = serializers.HyperlinkedRelatedField(view_name='user-detail', lookup_field='uuid',
                                               queryset=User.objects.all())

    project_name = serializers.Field(source='group.projectrole.project.name')
    user_full_name = serializers.Field(source='user.full_name')
    user_native_name = serializers.Field(source='user.native_name')

    role = ProjectRoleField(choices=models.ProjectRole.TYPE_CHOICES)

    class Meta(object):
        model = User.groups.through
        fields = (
            'url',
            'role',
            'project', 'project_name',
            'user', 'user_full_name', 'user_native_name',
        )
        view_name = 'project_permission-detail'

    def restore_object(self, attrs, instance=None):
        project = attrs['group.projectrole.project']
        group = project.roles.get(role_type=attrs['role']).permission_group
        UserGroup = User.groups.through
        return UserGroup(user=attrs['user'], group=group)

    def save_object(self, obj, **kwargs):
        try:
            obj.save()
        except IntegrityError:
            raise NotModifiedPermission()

    def get_filtered_field_names(self):
        return 'project',

class ProjectGroupPermissionSerializer(core_serializers.PermissionFieldFilteringMixin,
                                       serializers.HyperlinkedModelSerializer):
    project_group = serializers.HyperlinkedRelatedField(source='group.projectgrouprole.project_group', view_name='projectgroup-detail',
                                                        lookup_field='uuid', queryset=models.ProjectGroup.objects.all())
    user = serializers.HyperlinkedRelatedField(view_name='user-detail', lookup_field='uuid',
                                               queryset=User.objects.all())

    project_group_name = serializers.Field(source='group.projectgrouprole.project_group.name')
    user_full_name = serializers.Field(source='user.full_name')
    user_native_name = serializers.Field(source='user.native_name')

    role = ProjectGroupRoleField(choices=models.ProjectGroupRole.TYPE_CHOICES)

    class Meta(object):
        model = User.groups.through
        fields = (
            'url',
            'role',
            'project_group', 'project_group_name',
            'user', 'user_full_name', 'user_native_name',
        )
        view_name = 'projectgroup_permission-detail'

    def restore_object(self, attrs, instance=None):
        project_group = attrs['group.projectgrouprole.project_group']
        group = project_group.roles.get(role_type=attrs['role']).permission_group
        UserGroup = User.groups.through
        return UserGroup(user=attrs['user'], group=group)

    def save_object(self, obj, **kwargs):
        try:
            obj.save()
        except IntegrityError:
            raise NotModifiedPermission()

    def get_filtered_field_names(self):
        return 'project_group',

class UserSerializer(serializers.HyperlinkedModelSerializer):
    project_groups = serializers.SerializerMethodField('user_project_groups')
    email = serializers.EmailField()

    def user_project_groups(self, obj):
        request = self.context.get('request')

        project_groups_qs = models.ProjectGroup.objects.filter(
            projects__roles__permission_group__user=obj).distinct()

        return [
            {
                'url': reverse('projectgroup-detail', kwargs={'uuid': project_group['uuid']}, request=request),
                'name': project_group['name'],
            }
            for project_group in
            project_groups_qs.values('uuid', 'name').iterator()
        ]

    class Meta(object):
        model = User
        fields = ('url', 'uuid', 'username', 'full_name', 'native_name', 'job_title', 'email',
                  'civil_number', 'phone_number', 'description', 'is_staff', 'organization', 'project_groups')
        read_only_fields = ('uuid', 'is_staff')
        lookup_field = 'uuid'
<commit_msg>Make permission serializer user output consistent<commit_after>from __future__ import unicode_literals

from django.contrib import auth
from django.core.exceptions import ValidationError
from django.db import IntegrityError
from rest_framework import serializers
from rest_framework.exceptions import APIException
from rest_framework.reverse import reverse

from nodeconductor.core import serializers as core_serializers
from nodeconductor.structure import models
from nodeconductor.structure.filters import filter_queryset_for_user

User = auth.get_user_model()

class BasicUserSerializer(serializers.HyperlinkedModelSerializer):
    class Meta(object):
        model = User
        fields = ('url', 'uuid', 'username', 'full_name', 'native_name',)
        lookup_field = 'uuid'

class BasicProjectSerializer(core_serializers.BasicInfoSerializer):
    class Meta(core_serializers.BasicInfoSerializer.Meta):
        model = models.Project

class BasicProjectGroupSerializer(core_serializers.BasicInfoSerializer):
    class Meta(core_serializers.BasicInfoSerializer.Meta):
        model = models.ProjectGroup

class ResourceQuotaSerializer(serializers.ModelSerializer):
    class Meta(object):
        model = models.ResourceQuota
        fields = ('vcpu', 'ram', 'storage', 'max_instances')

class ProjectSerializer(core_serializers.CollectedFieldsMixin,
                        core_serializers.RelatedResourcesFieldMixin,
                        serializers.HyperlinkedModelSerializer):
    project_groups = BasicProjectGroupSerializer(many=True, read_only=True)
    resource_quota = ResourceQuotaSerializer(read_only=True)
    resource_quota_usage = ResourceQuotaSerializer(read_only=True)

    class Meta(object):
        model = models.Project
        fields = ('url', 'uuid', 'name', 'customer', 'customer_name', 'project_groups', 'resource_quota',
                  'resource_quota_usage')
        lookup_field = 'uuid'

    def get_related_paths(self):
        return 'customer',

class ProjectCreateSerializer(core_serializers.PermissionFieldFilteringMixin,
                              serializers.HyperlinkedModelSerializer):

    resource_quota = ResourceQuotaSerializer(required=False)

    class Meta(object):
        model = models.Project
        fields = ('url', 'name', 'customer', 'resource_quota', 'project_groups')
        lookup_field = 'uuid'

    def get_filtered_field_names(self):
        return 'customer',

    def validate(self, attrs):
        try:
            user = self.context['request'].user
        except (KeyError, AttributeError):
            return attrs

        if user.is_staff:
            return attrs
        is_customer_owner = attrs['customer'].roles.filter(
            permission_group__user=user, role_type=models.CustomerRole.OWNER).exists()
        if is_customer_owner:
            return attrs

        groups = attrs['project_groups']
        if not groups:
            raise ValidationError('Project has to belong to some group')

        for group in groups:
            is_group_manager = group.roles.filter(
                permission_group__user=user, role_type=models.ProjectGroupRole.MANAGER).exists()
            if not is_group_manager:
                raise ValidationError('User does not have permission to add project to %s' % group)

        return attrs

class CustomerSerializer(core_serializers.CollectedFieldsMixin,
                         serializers.HyperlinkedModelSerializer):
    projects = serializers.SerializerMethodField('get_customer_projects')
    project_groups = serializers.SerializerMethodField('get_customer_project_groups')
    owners = BasicUserSerializer(source='get_owners', many=True, read_only=True)

    class Meta(object):
        model = models.Customer
        fields = ('url', 'uuid', 'name', 'abbreviation', 'contact_details', 'projects', 'project_groups', 'owners')
        lookup_field = 'uuid'

    def _get_filtered_data(self, objects, serializer):
        try:
            user = self.context['request'].user
        except (KeyError, AttributeError):
            return None

        queryset = filter_queryset_for_user(objects, user)
        serializer_instance = serializer(queryset, context={'request': self.context['request']})
        return serializer_instance.data

    def get_customer_projects(self, obj):
        return self._get_filtered_data(obj.projects.all(), BasicProjectSerializer)

    def get_customer_project_groups(self, obj):
        return self._get_filtered_data(obj.project_groups.all(), BasicProjectGroupSerializer)

class ProjectGroupSerializer(core_serializers.PermissionFieldFilteringMixin,
                             core_serializers.RelatedResourcesFieldMixin,
                             serializers.HyperlinkedModelSerializer):
    projects = BasicProjectSerializer(many=True, read_only=True)

    class Meta(object):
        model = models.ProjectGroup
        fields = ('url', 'uuid', 'name', 'customer', 'customer_name', 'projects')
        lookup_field = 'uuid'

    def get_filtered_field_names(self):
        return 'customer',

    def get_fields(self):
        # TODO: Extract to a proper mixin
        fields = super(ProjectGroupSerializer, self).get_fields()

        try:
            method = self.context['view'].request.method
        except (KeyError, AttributeError):
            return fields

        if method in ('PUT', 'PATCH'):
            fields['customer'].read_only = True

        return fields

    def get_related_paths(self):
        return 'customer',

class ProjectGroupMembershipSerializer(core_serializers.PermissionFieldFilteringMixin,
                                       serializers.HyperlinkedModelSerializer):
    project_group = serializers.HyperlinkedRelatedField(
        source='projectgroup',
        view_name='projectgroup-detail',
        lookup_field='uuid',
    )
    project_group_name = serializers.Field(source='projectgroup.name')
    project = serializers.HyperlinkedRelatedField(
        view_name='project-detail',
        lookup_field='uuid',
    )
    project_name = serializers.Field(source='project.name')

    class Meta(object):
        model = models.ProjectGroup.projects.through
        fields = (
            'url',
            'project_group', 'project_group_name',
            'project', 'project_name',
        )
        view_name = 'projectgroup_membership-detail'

    def get_filtered_field_names(self):
        return 'project', 'project_group'

class ProjectRoleField(serializers.ChoiceField):

    def field_to_native(self, obj, field_name):
        if obj is not None:
            return models.ProjectRole.ROLE_TO_NAME[obj.group.projectrole.role_type]

    def field_from_native(self, data, files, field_name, into):
        role = data.get('role')
        if role in models.ProjectRole.NAME_TO_ROLE:
            into[field_name] = models.ProjectRole.NAME_TO_ROLE[role]
        else:
            raise ValidationError('Unknown role')

class ProjectGroupRoleField(serializers.ChoiceField):

    def field_to_native(self, obj, field_name):
        if obj is not None:
            return models.ProjectGroupRole.ROLE_TO_NAME[obj.group.projectgrouprole.role_type]

    def field_from_native(self, data, files, field_name, into):
        role = data.get('role')
        if role in models.ProjectGroupRole.NAME_TO_ROLE:
            into[field_name] = models.ProjectGroupRole.NAME_TO_ROLE[role]
        else:
            raise ValidationError('Unknown role')

class CustomerRoleField(serializers.ChoiceField):

    def field_to_native(self, obj, field_name):
        if obj is not None:
            return models.CustomerRole.ROLE_TO_NAME[obj.group.customerrole.role_type]

    def field_from_native(self, data, files, field_name, into):
        role = data.get('role')
        if role in models.CustomerRole.NAME_TO_ROLE:
            into[field_name] = models.CustomerRole.NAME_TO_ROLE[role]
        else:
            raise ValidationError('Unknown role')

class ProjectPermissionReadSerializer(core_serializers.RelatedResourcesFieldMixin,
                                      serializers.HyperlinkedModelSerializer):
    user = serializers.HyperlinkedRelatedField(
        view_name='user-detail',
        lookup_field='uuid',
        queryset=User.objects.all(),
    )
    user_full_name = serializers.Field(source='user.full_name')
    user_native_name = serializers.Field(source='user.native_name')

    role = ProjectRoleField(choices=models.ProjectRole.TYPE_CHOICES)

    class Meta(object):
        model = User.groups.through
        fields = (
            'url',
            'project', 'project_name',
            'user', 'user_full_name', 'user_native_name',
            'role',
        )
        view_name = 'project_permission-detail'

    def get_related_paths(self):
        return 'group.projectrole.project',

class NotModifiedPermission(APIException):
    status_code = 304
    default_detail = 'Permissions were not modified'

# TODO: refactor to abstract class, subclass by CustomerPermissions, ProjectPermissions and ProjectGroupPermissions
class CustomerPermissionSerializer(core_serializers.PermissionFieldFilteringMixin,
                                   serializers.HyperlinkedModelSerializer):
    customer = serializers.HyperlinkedRelatedField(
        source='group.customerrole.customer',
        view_name='customer-detail',
        lookup_field='uuid',
        queryset=models.Customer.objects.all(),
    )
    customer_name = serializers.Field(source='group.customerrole.customer.name')

    user = serializers.HyperlinkedRelatedField(
        view_name='user-detail',
        lookup_field='uuid',
        queryset=User.objects.all(),
    )
    user_full_name = serializers.Field(source='user.full_name')
    user_native_name = serializers.Field(source='user.native_name')
    user_username = serializers.Field(source='user.username')

    role = CustomerRoleField(choices=models.CustomerRole.TYPE_CHOICES)

    class Meta(object):
        model = User.groups.through
        fields = (
            'url', 'role',
            'customer', 'customer_name',
            'user', 'user_full_name', 'user_native_name', 'user_username',
        )
        view_name = 'customer_permission-detail'

    def restore_object(self, attrs, instance=None):
        customer = attrs['group.customerrole.customer']
        group = customer.roles.get(role_type=attrs['role']).permission_group
        UserGroup = User.groups.through
        return UserGroup(user=attrs['user'], group=group)

    def save_object(self, obj, **kwargs):
        try:
            obj.save()
        except IntegrityError:
            raise NotModifiedPermission()

    def get_filtered_field_names(self):
        return 'customer',

class ProjectPermissionSerializer(core_serializers.PermissionFieldFilteringMixin,
                                  serializers.HyperlinkedModelSerializer):
    project = serializers.HyperlinkedRelatedField(source='group.projectrole.project', view_name='project-detail',
                                                  lookup_field='uuid', queryset=models.Project.objects.all())
    user = serializers.HyperlinkedRelatedField(view_name='user-detail', lookup_field='uuid',
                                               queryset=User.objects.all())

    project_name = serializers.Field(source='group.projectrole.project.name')
    user_full_name = serializers.Field(source='user.full_name')
    user_native_name = serializers.Field(source='user.native_name')
    user_username = serializers.Field(source='user.username')

    role = ProjectRoleField(choices=models.ProjectRole.TYPE_CHOICES)

    class Meta(object):
        model = User.groups.through
        fields = (
            'url',
            'role',
            'project', 'project_name',
            'user', 'user_full_name', 'user_native_name', 'user_username',
        )
        view_name = 'project_permission-detail'

    def restore_object(self, attrs, instance=None):
        project = attrs['group.projectrole.project']
        group = project.roles.get(role_type=attrs['role']).permission_group
        UserGroup = User.groups.through
        return UserGroup(user=attrs['user'], group=group)

    def save_object(self, obj, **kwargs):
        try:
            obj.save()
        except IntegrityError:
            raise NotModifiedPermission()

    def get_filtered_field_names(self):
        return 'project',

class ProjectGroupPermissionSerializer(core_serializers.PermissionFieldFilteringMixin,
                                       serializers.HyperlinkedModelSerializer):
    project_group = serializers.HyperlinkedRelatedField(source='group.projectgrouprole.project_group', view_name='projectgroup-detail',
                                                        lookup_field='uuid', queryset=models.ProjectGroup.objects.all())
    user = serializers.HyperlinkedRelatedField(view_name='user-detail', lookup_field='uuid',
                                               queryset=User.objects.all())

    project_group_name = serializers.Field(source='group.projectgrouprole.project_group.name')
    user_full_name = serializers.Field(source='user.full_name')
    user_native_name = serializers.Field(source='user.native_name')
    user_username = serializers.Field(source='user.username')

    role = ProjectGroupRoleField(choices=models.ProjectGroupRole.TYPE_CHOICES)

    class Meta(object):
        model = User.groups.through
        fields = (
            'url',
            'role',
            'project_group', 'project_group_name',
            'user', 'user_full_name', 'user_native_name', 'user_username',
        )
        view_name = 'projectgroup_permission-detail'

    def restore_object(self, attrs, instance=None):
        project_group = attrs['group.projectgrouprole.project_group']
        group = project_group.roles.get(role_type=attrs['role']).permission_group
        UserGroup = User.groups.through
        return UserGroup(user=attrs['user'], group=group)

    def save_object(self, obj, **kwargs):
        try:
            obj.save()
        except IntegrityError:
            raise NotModifiedPermission()

    def get_filtered_field_names(self):
        return 'project_group',

class UserSerializer(serializers.HyperlinkedModelSerializer):
    project_groups = serializers.SerializerMethodField('user_project_groups')
    email = serializers.EmailField()

    def user_project_groups(self, obj):
        request = self.context.get('request')

        project_groups_qs = models.ProjectGroup.objects.filter(
            projects__roles__permission_group__user=obj).distinct()

        return [
            {
                'url': reverse('projectgroup-detail', kwargs={'uuid': project_group['uuid']}, request=request),
                'name': project_group['name'],
            }
            for project_group in
            project_groups_qs.values('uuid', 'name').iterator()
        ]

    class Meta(object):
        model = User
        fields = ('url', 'uuid', 'username', 'full_name', 'native_name', 'job_title', 'email',
                  'civil_number', 'phone_number', 'description', 'is_staff', 'organization', 'project_groups')
        read_only_fields = ('uuid', 'is_staff')
        lookup_field = 'uuid'
<|endoftext|>


<commit_before>"""
Author: Armon Dadgar
Start Date: March 31st, 2009
Description:
  Provides a tool-chain for altering resource files.

"""

# Finds all resource files
import glob

# Used for moving files
import os

def read_restrictions_file(file):
  """
  <Purpose>
    Reads in the contents of a restrictions file.

  <Arguments>
    file: name/path of the file to open

  <Returns>
    A list, where each element is a line in the file
  """
  # Get the file object, read mode with universal newlines
  fileo = open(file,"ru")

  # Read in all the contents
  contents = fileo.readlines()

  # Close the file object
  fileo.close()

  return contents

def write_restrictions_file(file, buffer):
  """
  <Purpose>
    Writes in the contents of a restrictions file. Tries to do this safely,
    by writing to a new file, backing-up the original, renaming the new file, and finally deleting the backup.

  <Arguments>
    file: name/path of the file to open
    buffer: A list, where each element is a line in the file

  <Returns>
    Nothing
  """
  # Get the file object, write mode
  # Use a .new suffix, so as not to corrupt the current file
  fileo = open(file+".new", "w")

  # Write in all the buffer
  for line in buffer:
    fileo.write(line+"\n")

  # Close the file object
  fileo.close()

  # Move the original to *.bak if it exists
  if os.path.exists(file):
    os.rename(file, file+".bak")

  # Move the new one to the original
  os.rename(file+".new", file)

  # Cleanup, remove the backup
  if os.path.exists(file+".bak"):
    os.remove(file+".bak")

def update_restriction(lines, restriction, restype, val, func=False):
  """
  <Purpose>
    Updates a resource in a restrictions file

  <Arguments>
    lines: The contents of the restrictions file, list each element is a line
    restriction: The name of the restriction e.g. resource, call
    restype: The type of restriction, e.g. events
    val: Either a new absolute value, or a callback function
    func: If true, this indicates that val is a function. That function will be given a list of the elements on the current line, and is expected
    to return the appropriate val to use.

  <Side Effects>
    The restrictions file will be modified

  <Returns>
    The contents of the new restrictions file, list each element is a line
  """
  # Empty buffer for new contents
  newContents = []

  # Check each line if it contains the resource
  for line in lines:
    if restriction in line:
      # Explode on space
      lineContents = line.split(" ")

      # Make sure this is the correct resource
      try:
        assert(lineContents[0] == restriction)
        assert(lineContents[1] == restype)
      except AssertionError:
        # Wrong line, continue after appending this line
        newContents.append(line)
        continue

      # Check if we are using a callback function
      if func:
        userVal = val(lineContents)
      else:
        # Otherwise, this is just the real value
        userVal = val

      # Change the value to the string val
      lineContents[2] = str(userVal)

      # Re-create the line string, with the modifications
      lineString = ""
      for elem in lineContents:
        lineString += elem + " "
      lineString = lineString.strip()

      # Append the new line to the buffer
      newContents.append(lineString)

    else:
      # Just append this line to the buffer
      newContents.append(line)

  # Return the modified buffer
  return newContents

def get_all_resource_files():
  """
  <Purpose>
    Returns a list with all the resource files.

  <Returns>
    A list object with file names.
  """
  return glob.glob("resource.v*")

def process_restriction_file(file, tasks):
  """
  <Purpose>
    Serves as a useful macro for processing a resource file.

  <Argument>
    file: The name of a resource file.
    tasks: A list of tuple objects. Each tuple should contain the following:
    (restriction, restype, val, func). See update_restriction for the meaning of these values.

  <Returns>
    Nothing
  """
  # Get the content
  content = read_restrictions_file(file)

  # Create a new buffer to store the changes
  newContent = content

  # Run each task against the restrictions file
  for task in tasks:
    (restriction, restype, val, func) = task
    newContent = update_restriction(newContent, restriction, restype, val, func)

  # Check if there were any changes
  if content != newContent:
    # Write out the changes
    write_restrictions_file(file, newContent)

def process_all_files(tasks):
  """
  <Purpose>
    Serves as a useful macro for processing all resource files.

  <Arguments>
    tasks: A list of tuple objects. Each tuple should contain the following:
    (restriction, restype, val, func). See update_restriction for the meaning of these values.

  <Returns>
    A list of all the failures. They are in the form of tuples: (file, exception)
  """
  # Get all the resource files
  allFiles = get_all_resource_files()

  # Stores the name of all files that failed
  failedFiles = []

  # Process each one
  for rFile in allFiles:
    try:
      # Process this file
      process_restriction_file(rFile, tasks)

    # Log if any files fail
    except Exception, exp:
      failedFiles.append((rFile, exp))

  # Return the list of failed files
  return failedFiles

<commit_msg>Removed asserts and used plain if statements. Also fixed bug with extra newline characters.<commit_after>"""
Author: Armon Dadgar
Start Date: March 31st, 2009
Description:
  Provides a tool-chain for altering resource files.

"""

# Finds all resource files
import glob

# Used for moving files
import os

def read_restrictions_file(file):
  """
  <Purpose>
    Reads in the contents of a restrictions file.

  <Arguments>
    file: name/path of the file to open

  <Returns>
    A list, where each element is a line in the file
  """
  # Get the file object, read mode with universal newlines
  fileo = open(file,"ru")

  # Read in all the contents
  contents = fileo.readlines()

  # Close the file object
  fileo.close()

  return contents

def write_restrictions_file(file, buffer):
  """
  <Purpose>
    Writes in the contents of a restrictions file. Tries to do this safely,
    by writing to a new file, backing-up the original, renaming the new file, and finally deleting the backup.

  <Arguments>
    file: name/path of the file to open
    buffer: A list, where each element is a line in the file

  <Returns>
    Nothing
  """
  # Get the file object, write mode
  # Use a .new suffix, so as not to corrupt the current file
  fileo = open(file+".new", "w")

  # Write in all the buffer
  for line in buffer:
    fileo.write(line)

  # Close the file object
  fileo.close()

  # Move the original to *.bak if it exists
  if os.path.exists(file):
    os.rename(file, file+".bak")

  # Move the new one to the original
  os.rename(file+".new", file)

  # Cleanup, remove the backup
  if os.path.exists(file+".bak"):
    os.remove(file+".bak")

def update_restriction(lines, restriction, restype, val, func=False):
  """
  <Purpose>
    Updates a resource in a restrictions file

  <Arguments>
    lines: The contents of the restrictions file, list each element is a line
    restriction: The name of the restriction e.g. resource, call
    restype: The type of restriction, e.g. events
    val: Either a new absolute value, or a callback function
    func: If true, this indicates that val is a function. That function will be given a list of the elements on the current line, and is expected
    to return the appropriate val to use.

  <Side Effects>
    The restrictions file will be modified

  <Returns>
    The contents of the new restrictions file, list each element is a line
  """
  # Empty buffer for new contents
  newContents = []

  # Store the length of the restriction
  restrictionLength = len(restriction)

  # Check each line if it contains the resource
  for line in lines:
    # Check if the line starts with a comment or the restriction as an optimization
    # This prevents us from processing every single line needlessly
    if not line[0] == "#" and restriction == line[0:restrictionLength]:
      # Explode on space
      lineContents = line.split(" ")

      # Make sure this is the correct resource
      # This is okay if there are comments, because either the index will be offset
      # Or the value of the index will be changed, and will not match
      if not lineContents[0] == restriction or not lineContents[1] == restype:
        # Wrong line, continue after appending this line
        newContents.append(line)
        continue

      # Check if we are using a callback function
      if func:
        userVal = val(lineContents)
      else:
        # Otherwise, this is just the real value
        userVal = val

      # Change the value to the string val
      lineContents[2] = str(userVal)

      # Re-create the line string, with the modifications
      lineString = ""
      for elem in lineContents:
        lineString += elem + " "
      lineString = lineString.strip()
      lineString += "\n"

      # Append the new line to the buffer
      newContents.append(lineString)

    else:
      # Just append this line to the buffer
      newContents.append(line)

  # Return the modified buffer
  return newContents

def get_all_resource_files():
  """
  <Purpose>
    Returns a list with all the resource files.

  <Returns>
    A list object with file names.
  """
  return glob.glob("resource.v*")

def process_restriction_file(file, tasks):
  """
  <Purpose>
    Serves as a useful macro for processing a resource file.

  <Argument>
    file: The name of a resource file.
    tasks: A list of tuple objects. Each tuple should contain the following:
    (restriction, restype, val, func). See update_restriction for the meaning of these values.

  <Returns>
    Nothing
  """
  # Get the content
  content = read_restrictions_file(file)

  # Create a new buffer to store the changes
  newContent = content

  # Run each task against the restrictions file
  for task in tasks:
    (restriction, restype, val, func) = task
    newContent = update_restriction(newContent, restriction, restype, val, func)

  # Check if there were any changes
  if content != newContent:
    # Write out the changes
    write_restrictions_file(file, newContent)

def process_all_files(tasks):
  """
  <Purpose>
    Serves as a useful macro for processing all resource files.

  <Arguments>
    tasks: A list of tuple objects. Each tuple should contain the following:
    (restriction, restype, val, func). See update_restriction for the meaning of these values.

  <Returns>
    A list of all the failures. They are in the form of tuples: (file, exception)
  """
  # Get all the resource files
  allFiles = get_all_resource_files()

  # Stores the name of all files that failed
  failedFiles = []

  # Process each one
  for rFile in allFiles:
    try:
      # Process this file
      process_restriction_file(rFile, tasks)

    # Log if any files fail
    except Exception, exp:
      failedFiles.append((rFile, exp))

  # Return the list of failed files
  return failedFiles

<|endoftext|>


<commit_before>from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import os.path

import numpy as np
from numpy.testing import assert_allclose

from abel.hansenlaw import  iabel_hansenlaw
from abel.analytical import GaussianAnalytical
from abel.benchmark import absolute_ratio_benchmark

def test_hansenlaw_shape():
    n = 21
    x = np.ones((n, n), dtype='float32')

    recon = iabel_hansenlaw(x, calc_speeds=False, verbose=False)

    assert recon.shape == (n, n)

def test_hansenlaw_zeros():
    n = 21
    x = np.zeros((n, n), dtype='float32')

    recon = iabel_hansenlaw(x, calc_speeds=False, verbose=False)

    assert_allclose(recon, 0)

def test_hansenlaw_gaussian():
    """Check a gaussian solution for HansenLaw"""
    n = 1001   # better with a larger number of points
    r_max = 501

    ref = GaussianAnalytical(n, r_max, symmetric=True,  sigma=200)
    tr = np.tile(ref.abel[None, :], (n, 1)) # make a 2D array from 1D

    recon = iabel_hansenlaw(tr)
    recon1d = recon[n//2 + n%2]  # centre row

    ratio = absolute_ratio_benchmark(ref, recon1d)

    assert_allclose(ratio,  1.0, rtol=0.1, atol=0)
<commit_msg>test_hansenlaw.py: Curve A unit test of hansenlaw_transform()<commit_after>from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import os.path

import numpy as np
from numpy.testing import assert_allclose

from abel.hansenlaw import  iabel_hansenlaw, iabel_hansenlaw_transform
from abel.analytical import GaussianAnalytical
from abel.benchmark import absolute_ratio_benchmark

def test_hansenlaw_shape():
    n = 21
    x = np.ones((n, n), dtype='float32')

    recon = iabel_hansenlaw(x, calc_speeds=False, verbose=False)

    assert recon.shape == (n, n)

def test_hansenlaw_zeros():
    n = 21
    x = np.zeros((n, n), dtype='float32')

    recon = iabel_hansenlaw(x, calc_speeds=False, verbose=False)

    assert_allclose(recon, 0)

def test_hansenlaw_gaussian():
    """Check a gaussian solution for HansenLaw"""
    n = 1001   # better with a larger number of points
    r_max = 501

    ref = GaussianAnalytical(n, r_max, symmetric=True,  sigma=200)
    tr = np.tile(ref.abel[None, :], (n, 1)) # make a 2D array from 1D

    recon = iabel_hansenlaw(tr)
    recon1d = recon[n//2 + n%2]  # centre row

    ratio = absolute_ratio_benchmark(ref, recon1d)

    assert_allclose(ratio,  1.0, rtol=0.1, atol=0)

def test_hansenlaw_curveA():
    """ Check hansenlaw_transform()
        verify curve A, Table 2, Fig. 3, JOSA A2 510 (1985).
    """
    # Abel transform pair
    def f (r):
        return 1-2*r**2 if np.all(r) <= 0.5 else 2*(1-r)**2

    def g (R):
        alpha = np.sqrt(1-R**2)
        R2 = R**2

        if np.all(R) <= 0.5:
            beta  = np.sqrt(0.25-R**2)
            return (2/3)*(2*alpha*(1+2*R2)-beta*(1+8*R2))-\
                   4*R2*np.log((1+alpha)/(0.5+beta))
        else:
            return (4/3)*alpha*(1+2*R2)-4*R2*np.log((1+alpha)/R)

    delta = 0.01
    # split r-range to suit function pair
    # there must be a more Python way to achieve this
    rl = np.arange(0,0.5+delta/2,delta) # 0 <= r <= 0.5
    rr = np.arange(0.5+delta,1.0,delta) # 0.5 < r < 1.0
    r = np.concatenate((rl,rr),axis=0)  # r = [0,1)

    orig = np.concatenate((f(rl),f(rr)),axis=0)   # f(r)
    proj = np.concatenate((g(rl),g(rr)),axis=0)   # g(r)
    orig = orig[::-1] # flip them
    proj = proj[::-1]

    recon = iabel_hansenlaw_transform(proj)[0]  # inverse Abel
                                                # == g(r)???
    orig = orig[::-1]  # flip back
    recon = recon[::-1]/delta

    mask = r > 0.1  # check deviation away from "image centre"
    diff = np.abs(recon[mask]-orig[mask])
    assert_allclose(diff, 0.0, atol=0.011)

<|endoftext|>


<commit_before>from zeit.content.image.testing import create_image_group_with_master_image
import gocept.jasmine.jasmine
import json
import requests
import transaction
import zeit.cms.repository.interfaces
import zeit.cms.testing
import zeit.content.image.interfaces
import zeit.content.image.testing
import zope.component

class VariantJsonAPI(zeit.cms.testing.FunctionalTestCase):
    """Test JSON API to retrieve all Variants or a single Variant.

    We use the requests library instead of a BrowserTestCase, since
    zope.testbrowser supports GET and POST, but not PUT and DELETE.

    """

    layer = zeit.content.image.testing.HTTP_LAYER

    def setUp(self):
        super(VariantJsonAPI, self).setUp()
        self.group = create_image_group_with_master_image()
        self.group.variants = {
            'square': {'focus_x': 0.5, 'focus_y': 0.5, 'zoom': 1.0}
        }
        transaction.commit()

    def request(self, method, path, **kw):
        response = getattr(requests, method)('http://%s/++skin++vivi%s' % (
            self.layer['http_address'], path), auth=('user', 'userpw'), **kw)
        if response.status_code >= 400:
            raise ValueError('HTTP status %s:\n%s' % (
                response.status_code, response.text))
        return response

    def test_list_variants_excludes_default(self):
        r = self.request('get', '/repository/group/variants')
        self.assertEqual(['cinema-small', 'cinema-large', 'square'],
                         [x['id'] for x in r.json()])

    def test_get_variant_contains_configuration_of_imagegroup_and_xml(self):
        variant = self.request(
            'get', '/workingcopy/zope.user/group/variants/square').json()
        self.assertEqual(0.5, variant['focus_x'])    # from imagegroup
        self.assertEqual(1, variant['zoom'])         # from default variant
        self.assertEqual('square', variant['name'])  # from XML

    def test_list_variants_ignores_keys_that_are_only_stored_in_config_dict(
            self):
        self.group.variants = {'foobarbaz': {'focus_x': 0.1, 'focus_y': 0.1}}
        transaction.commit()
        r = self.request('get', '/repository/group/variants')
        self.assertNotIn('foobarbaz', [x['id'] for x in r.json()])

    def test_get_variant_contains_all_fields_defined_on_interface_and_url(
            self):
        r = self.request('get', '/repository/group/variants/square')
        variant = r.json()
        self.assertEqual(0.5, variant['focus_x'])
        self.assertEqual(0.5, variant['focus_y'])
        self.assertEqual(
            sorted(['url'] + list(zeit.content.image.interfaces.IVariant)),
            sorted(variant.keys()))

    def test_put_variant_stores_focuspoint_zoom_and_image_enhancements_only(
            self):
        # All other attributes are transient or come from XML
        fields = zope.schema.getFields(zeit.content.image.interfaces.IVariant)
        data = {}
        for key in fields.keys():
            data[key] = 1

        self.request(
            'put', '/repository/group/variants/square', data=json.dumps(data))
        transaction.abort()
        self.assertEqual(
            sorted(['brightness', 'contrast', 'saturation', 'sharpness',
                    'focus_x', 'focus_y', 'zoom']),
            sorted(self.group.variants['square'].keys()))

    def test_put_variant_stores_value_of_focuspoint_zoom_and_img_enhancements(
            self):
        data = {
            'brightness': 0.5,
            'contrast': 0.5,
            'saturation': 0.5,
            'sharpness': 0.5,
            'focus_x': 0.1,
            'focus_y': 0.2,
            'zoom': 0.1
        }
        self.request(
            'put', '/repository/group/variants/square',
            data=json.dumps(data))
        transaction.abort()
        self.assertEqual(data, self.group.variants['square'])

    def test_put_variant_returns_error_if_focuspoint_was_set_to_None(self):
        # Setting focuspoint to None would break Image generation
        data = self.group.variants['square'].copy()
        data['focus_x'] = data['focus_y'] = None
        with self.assertRaises(ValueError):
            self.request(
                'put', '/repository/group/variants/square',
                data=json.dumps(data))
        transaction.abort()
        self.assertEqual(
            {'focus_x': 0.5, 'focus_y': 0.5, 'zoom': 1.0},
            self.group.variants['square'])

    def test_put_variant_returns_error_if_zoom_was_set_to_None(self):
        # Setting zoom to None would break Image generation
        data = self.group.variants['square'].copy()
        data['zoom'] = None
        with self.assertRaises(ValueError):
            self.request(
                'put', '/repository/group/variants/square',
                data=json.dumps(data))
        transaction.abort()
        self.assertEqual(
            {'focus_x': 0.5, 'focus_y': 0.5, 'zoom': 1.0},
            self.group.variants['square'])

    def test_put_variant_ignores_None_values_for_image_enhancements(
            self):
        # None is the default for image enhancements and means 'is not used'.
        # Setting it explicitly would cause issues when converting to float.
        data = self.group.variants['square'].copy()
        data['brightness'] = None
        data['contrast'] = None
        data['saturation'] = None
        data['sharpness'] = None
        with self.assertNothingRaised():
            self.request(
                'put', '/repository/group/variants/square',
                data=json.dumps(data))
        transaction.abort()
        self.assertEqual(
            {'focus_x': 0.5, 'focus_y': 0.5, 'zoom': 1.0},
            self.group.variants['square'])

    def test_delete_removes_variant_config_from_group(self):
        self.request('delete', '/repository/group/variants/square')
        self.assertEqual({}, self.group.variants)

    def test_delete_for_variant_without_config_succeeds_anyway(self):
        with self.assertNothingRaised():
            self.request('delete', '/repository/group/variants/cinema-small')

class VariantIntegrationTest(zeit.cms.testing.SeleniumTestCase):

    layer = zeit.content.image.testing.WEBDRIVER_LAYER
    window_width = 1400  # The "Variants" tab needs to fit in and be clickable.

    def test_integration(self):
        """Open Image group and change settings of master and a variant."""
        s = self.selenium
        self.open('/repository/2007/03/group')
        s.click('link=Variants')
        s.waitForCssCount('css=#variant-content', 1)  # wait for tab to load

        # test that descriptive name is displayed near image
        s.assertTextPresent('Vollbreit (L)')

        # change default
        s.dragAndDrop('css=.zoom-bar .ui-slider-handle', '0,-50')
        s.dragAndDrop('css=.focuspoint', '50,50')
        s.waitForCssCount('css=.saved', 1)
        s.waitForCssCount('css=.saved', 0)

        # switch to first preview, i.e. cinema-small
        s.click('css=img.preview')
        s.waitForCssCount('css=.switched', 1)
        s.waitForCssCount('css=.switched', 0)

        # change settings for cinema-small
        s.dragAndDrop('css=.cropper-point.point-nw', '50,50')
        s.waitForCssCount('css=.saved', 1)
        s.waitForCssCount('css=.saved', 0)

        repository = zope.component.getUtility(
            zeit.cms.repository.interfaces.IRepository)
        variants = repository['2007']['03']['group'].variants
        self.assertEqual(['cinema-small', 'default'], sorted(variants.keys()))
        # Compare Zoom values: cinema-small < default < 1 (default setting)
        self.assertLess(variants['default']['zoom'], 1)
        self.assertLess(
            variants['cinema-small']['zoom'],
            variants['default']['zoom'])
        # Compare Focus Point: cinema-small > default > 0.5 (default setting)
        self.assertGreater(variants['default']['focus_x'], 0.5)
        self.assertGreater(
            variants['cinema-small']['focus_x'],
            variants['default']['focus_x'])

        # Make sure "Verwerfen" deletes config of the cinema-small variant
        s.click('css=input[value=Verwerfen]')
        s.waitForCssCount('css=.reset_single', 1)
        s.waitForCssCount('css=.reset_single', 0)

        repository = zope.component.getUtility(
            zeit.cms.repository.interfaces.IRepository)
        variants = repository['2007']['03']['group'].variants
        self.assertEqual(['default'], sorted(variants.keys()))

        # Slider should change value of image enhancements
        variants = repository['2007']['03']['group'].variants
        self.assertNotIn('brightness', variants['default'].keys())

        s.dragAndDrop('css=.brightness-bar .ui-slider-handle', '-50,0')
        s.waitForCssCount('css=.saved', 1)
        s.waitForCssCount('css=.saved', 0)

        variants = repository['2007']['03']['group'].variants
        self.assertIn('brightness', variants['default'].keys())
        self.assertGreater(1, variants['default']['brightness'])

class VariantApp(gocept.jasmine.jasmine.TestApp):

    def need_resources(self):
        zeit.content.image.browser.resources.test_variant_js.need()

class VariantJasmineTestCase(gocept.jasmine.jasmine.TestCase):

    layer = gocept.jasmine.jasmine.get_layer(VariantApp())
    level = 2

    def test_all_jasmine_unit_tests_run_successfully(self):
        self.run_jasmine()
<commit_msg>Use URL to repository, since we changed Variant editor to work on repository content a long time ago.<commit_after>from zeit.content.image.testing import create_image_group_with_master_image
import gocept.jasmine.jasmine
import json
import requests
import transaction
import zeit.cms.repository.interfaces
import zeit.cms.testing
import zeit.content.image.interfaces
import zeit.content.image.testing
import zope.component

class VariantJsonAPI(zeit.cms.testing.FunctionalTestCase):
    """Test JSON API to retrieve all Variants or a single Variant.

    We use the requests library instead of a BrowserTestCase, since
    zope.testbrowser supports GET and POST, but not PUT and DELETE.

    """

    layer = zeit.content.image.testing.HTTP_LAYER

    def setUp(self):
        super(VariantJsonAPI, self).setUp()
        self.group = create_image_group_with_master_image()
        self.group.variants = {
            'square': {'focus_x': 0.5, 'focus_y': 0.5, 'zoom': 1.0}
        }
        transaction.commit()

    def request(self, method, path, **kw):
        response = getattr(requests, method)('http://%s/++skin++vivi%s' % (
            self.layer['http_address'], path), auth=('user', 'userpw'), **kw)
        if response.status_code >= 400:
            raise ValueError('HTTP status %s:\n%s' % (
                response.status_code, response.text))
        return response

    def test_list_variants_excludes_default(self):
        r = self.request('get', '/repository/group/variants')
        self.assertEqual(['cinema-small', 'cinema-large', 'square'],
                         [x['id'] for x in r.json()])

    def test_get_variant_contains_configuration_of_imagegroup_and_xml(self):
        variant = self.request(
            'get', '/repository/group/variants/square').json()
        self.assertEqual(0.5, variant['focus_x'])    # from imagegroup
        self.assertEqual(1, variant['zoom'])         # from default variant
        self.assertEqual('square', variant['name'])  # from XML

    def test_list_variants_ignores_keys_that_are_only_stored_in_config_dict(
            self):
        self.group.variants = {'foobarbaz': {'focus_x': 0.1, 'focus_y': 0.1}}
        transaction.commit()
        r = self.request('get', '/repository/group/variants')
        self.assertNotIn('foobarbaz', [x['id'] for x in r.json()])

    def test_get_variant_contains_all_fields_defined_on_interface_and_url(
            self):
        r = self.request('get', '/repository/group/variants/square')
        variant = r.json()
        self.assertEqual(0.5, variant['focus_x'])
        self.assertEqual(0.5, variant['focus_y'])
        self.assertEqual(
            sorted(['url'] + list(zeit.content.image.interfaces.IVariant)),
            sorted(variant.keys()))

    def test_put_variant_stores_focuspoint_zoom_and_image_enhancements_only(
            self):
        # All other attributes are transient or come from XML
        fields = zope.schema.getFields(zeit.content.image.interfaces.IVariant)
        data = {}
        for key in fields.keys():
            data[key] = 1

        self.request(
            'put', '/repository/group/variants/square', data=json.dumps(data))
        transaction.abort()
        self.assertEqual(
            sorted(['brightness', 'contrast', 'saturation', 'sharpness',
                    'focus_x', 'focus_y', 'zoom']),
            sorted(self.group.variants['square'].keys()))

    def test_put_variant_stores_value_of_focuspoint_zoom_and_img_enhancements(
            self):
        data = {
            'brightness': 0.5,
            'contrast': 0.5,
            'saturation': 0.5,
            'sharpness': 0.5,
            'focus_x': 0.1,
            'focus_y': 0.2,
            'zoom': 0.1
        }
        self.request(
            'put', '/repository/group/variants/square',
            data=json.dumps(data))
        transaction.abort()
        self.assertEqual(data, self.group.variants['square'])

    def test_put_variant_returns_error_if_focuspoint_was_set_to_None(self):
        # Setting focuspoint to None would break Image generation
        data = self.group.variants['square'].copy()
        data['focus_x'] = data['focus_y'] = None
        with self.assertRaises(ValueError):
            self.request(
                'put', '/repository/group/variants/square',
                data=json.dumps(data))
        transaction.abort()
        self.assertEqual(
            {'focus_x': 0.5, 'focus_y': 0.5, 'zoom': 1.0},
            self.group.variants['square'])

    def test_put_variant_returns_error_if_zoom_was_set_to_None(self):
        # Setting zoom to None would break Image generation
        data = self.group.variants['square'].copy()
        data['zoom'] = None
        with self.assertRaises(ValueError):
            self.request(
                'put', '/repository/group/variants/square',
                data=json.dumps(data))
        transaction.abort()
        self.assertEqual(
            {'focus_x': 0.5, 'focus_y': 0.5, 'zoom': 1.0},
            self.group.variants['square'])

    def test_put_variant_ignores_None_values_for_image_enhancements(
            self):
        # None is the default for image enhancements and means 'is not used'.
        # Setting it explicitly would cause issues when converting to float.
        data = self.group.variants['square'].copy()
        data['brightness'] = None
        data['contrast'] = None
        data['saturation'] = None
        data['sharpness'] = None
        with self.assertNothingRaised():
            self.request(
                'put', '/repository/group/variants/square',
                data=json.dumps(data))
        transaction.abort()
        self.assertEqual(
            {'focus_x': 0.5, 'focus_y': 0.5, 'zoom': 1.0},
            self.group.variants['square'])

    def test_delete_removes_variant_config_from_group(self):
        self.request('delete', '/repository/group/variants/square')
        self.assertEqual({}, self.group.variants)

    def test_delete_for_variant_without_config_succeeds_anyway(self):
        with self.assertNothingRaised():
            self.request('delete', '/repository/group/variants/cinema-small')

class VariantIntegrationTest(zeit.cms.testing.SeleniumTestCase):

    layer = zeit.content.image.testing.WEBDRIVER_LAYER
    window_width = 1400  # The "Variants" tab needs to fit in and be clickable.

    def test_integration(self):
        """Open Image group and change settings of master and a variant."""
        s = self.selenium
        self.open('/repository/2007/03/group')
        s.click('link=Variants')
        s.waitForCssCount('css=#variant-content', 1)  # wait for tab to load

        # test that descriptive name is displayed near image
        s.assertTextPresent('Vollbreit (L)')

        # change default
        s.dragAndDrop('css=.zoom-bar .ui-slider-handle', '0,-50')
        s.dragAndDrop('css=.focuspoint', '50,50')
        s.waitForCssCount('css=.saved', 1)
        s.waitForCssCount('css=.saved', 0)

        # switch to first preview, i.e. cinema-small
        s.click('css=img.preview')
        s.waitForCssCount('css=.switched', 1)
        s.waitForCssCount('css=.switched', 0)

        # change settings for cinema-small
        s.dragAndDrop('css=.cropper-point.point-nw', '50,50')
        s.waitForCssCount('css=.saved', 1)
        s.waitForCssCount('css=.saved', 0)

        repository = zope.component.getUtility(
            zeit.cms.repository.interfaces.IRepository)
        variants = repository['2007']['03']['group'].variants
        self.assertEqual(['cinema-small', 'default'], sorted(variants.keys()))
        # Compare Zoom values: cinema-small < default < 1 (default setting)
        self.assertLess(variants['default']['zoom'], 1)
        self.assertLess(
            variants['cinema-small']['zoom'],
            variants['default']['zoom'])
        # Compare Focus Point: cinema-small > default > 0.5 (default setting)
        self.assertGreater(variants['default']['focus_x'], 0.5)
        self.assertGreater(
            variants['cinema-small']['focus_x'],
            variants['default']['focus_x'])

        # Make sure "Verwerfen" deletes config of the cinema-small variant
        s.click('css=input[value=Verwerfen]')
        s.waitForCssCount('css=.reset_single', 1)
        s.waitForCssCount('css=.reset_single', 0)

        repository = zope.component.getUtility(
            zeit.cms.repository.interfaces.IRepository)
        variants = repository['2007']['03']['group'].variants
        self.assertEqual(['default'], sorted(variants.keys()))

        # Slider should change value of image enhancements
        variants = repository['2007']['03']['group'].variants
        self.assertNotIn('brightness', variants['default'].keys())

        s.dragAndDrop('css=.brightness-bar .ui-slider-handle', '-50,0')
        s.waitForCssCount('css=.saved', 1)
        s.waitForCssCount('css=.saved', 0)

        variants = repository['2007']['03']['group'].variants
        self.assertIn('brightness', variants['default'].keys())
        self.assertGreater(1, variants['default']['brightness'])

class VariantApp(gocept.jasmine.jasmine.TestApp):

    def need_resources(self):
        zeit.content.image.browser.resources.test_variant_js.need()

class VariantJasmineTestCase(gocept.jasmine.jasmine.TestCase):

    layer = gocept.jasmine.jasmine.get_layer(VariantApp())
    level = 2

    def test_all_jasmine_unit_tests_run_successfully(self):
        self.run_jasmine()
<|endoftext|>