From 2bb8cd015a7ba893e9faaf5944fbbabd310a6a19 Mon Sep 17 00:00:00 2001 From: christopher lee Date: Thu, 10 Jul 2014 13:55:16 -0400 Subject: [PATCH] Initialized django project for VAL Created models for Video, EncodedVideo, and Profile. Added function to return a serialized video object, with it's associated EncodedVideo objects. Other Notes: -Added djangorestframework==2.3.5 -Added Travis -Added mock==1.0.1 -Added django-nose==1.2 -Added coverage==3.7.1 --- .gitignore | 66 ++++++++++++ edxval/__init__.py | 0 edxval/admin.py | 7 ++ edxval/api.py | 111 ++++++++++++++++++++ edxval/models.py | 80 +++++++++++++++ edxval/serializers.py | 44 ++++++++ edxval/settings.py | 167 +++++++++++++++++++++++++++++++ edxval/tests/__init__.py | 0 edxval/tests/constants.py | 56 +++++++++++ edxval/tests/test_api.py | 77 ++++++++++++++ edxval/tests/test_models.py | 3 + edxval/tests/test_serializers.py | 54 ++++++++++ edxval/urls.py | 17 ++++ edxval/wsgi.py | 28 ++++++ manage.py | 10 ++ requirements.txt | 2 + test-requirements.txt | 3 + 17 files changed, 725 insertions(+) create mode 100644 .gitignore create mode 100644 edxval/__init__.py create mode 100644 edxval/admin.py create mode 100644 edxval/api.py create mode 100644 edxval/models.py create mode 100644 edxval/serializers.py create mode 100644 edxval/settings.py create mode 100644 edxval/tests/__init__.py create mode 100644 edxval/tests/constants.py create mode 100644 edxval/tests/test_api.py create mode 100644 edxval/tests/test_models.py create mode 100644 edxval/tests/test_serializers.py create mode 100644 edxval/urls.py create mode 100644 edxval/wsgi.py create mode 100755 manage.py create mode 100644 requirements.txt create mode 100644 test-requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..6b79bca8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,66 @@ +*.py[cod] + +*.db + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib64 +__pycache__ + +# Various common editor backups +*.orig +.*.swp + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox +nosetests.xml +htmlcov +coverage.xml + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Sass/Codekit +.sass-cache/ +config.codekit + +# some mac thing +.DS_Store + +# PyCharm +.idea + +# node +node_modules +npm-debug.log +coverage + +# tim-specific +ora2db +storage/* +openassessment/xblock/static/js/fixtures/*.html + +# logging +logs/*/*.log* + +# Vagrant +.vagrant diff --git a/edxval/__init__.py b/edxval/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/edxval/admin.py b/edxval/admin.py new file mode 100644 index 00000000..8b5fb473 --- /dev/null +++ b/edxval/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from .models import Video, Profile, EncodedVideo + +admin.site.register(Video) +admin.site.register(Profile) +admin.site.register(EncodedVideo) + diff --git a/edxval/api.py b/edxval/api.py new file mode 100644 index 00000000..41c95ecd --- /dev/null +++ b/edxval/api.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +""" +The internal API for VAL +""" +import logging + +from edxval.models import Video +from edxval.serializers import EncodedVideoSetSerializer + +logger = logging.getLogger(__name__) + + +class ValError(Exception): + """ + An error that occurs during VAL actions. + + This error is raised when the VAL API cannot perform a requested + action. + + """ + pass + + +class ValInternalError(ValError): + """ + An error internal to the VAL API has occurred. + + This error is raised when an error occurs that is not caused by incorrect + use of the API, but rather internal implementation of the underlying + services. + + """ + pass + + +class ValVideoNotFoundError(ValError): + """ + This error is raised when a video is not found + + If a state is specified in a call to the API that results in no matching + entry in database, this error may be raised. + + """ + pass + + +def get_video_info(edx_video_id, location=None): + """ + Retrieves all encoded videos of a video found with given video edx_video_id + + Args: + location (str): geographic locations used determine CDN + edx_video_id (str): id for video content. + + Returns: + result (dict): Deserialized Video Object with related field EncodedVideo + Returns all the Video object fields, and it's related EncodedVideo + objects in a list. + { + edx_video_id: ID of the video + duration: Length of video in seconds + client_title: human readable ID + encoded_video: a list of EncodedVideo dicts + url: url of the video + file_size: size of the video in bytes + profile: a dict of encoding details + profile_name: ID of the profile + extension: 3 letter extension of video + width: horizontal pixel resolution + height: vertical pixel resolution + } + + Raises: + ValVideoNotFoundError: Raised if video doesn't exist + ValInternalError: Raised for unknown errors + + Example: + Given one EncodedVideo with edx_video_id "thisis12char-thisis7" + >>> + >>> get_video_info("thisis12char-thisis7",location) + Returns (dict): + >>>{ + >>> 'edx_video_id': u'thisis12char-thisis7', + >>> 'duration': 111.0, + >>> 'client_title': u'Thunder Cats S01E01', + >>> 'encoded_video': [ + >>> { + >>> 'url': u'http://www.meowmix.com', + >>> 'file_size': 25556, + >>> 'bitrate': 9600, + >>> 'profile': { + >>> 'profile_name': u'mobile', + >>> 'extension': u'avi', + >>> 'width': 100, + >>> 'height': 101 + >>> } + >>> }, + >>> ] + >>>} + """ + try: + v = Video.objects.get(edx_video_id=edx_video_id) + result = EncodedVideoSetSerializer(v) + except Video.DoesNotExist: + error_message = u"Video not found for edx_video_id: {0}".format(edx_video_id) + raise ValVideoNotFoundError(error_message) + except Exception: + error_message = u"Could not get edx_video_id: {0}".format(edx_video_id) + logger.exception(error_message) + raise ValInternalError(error_message) + return result.data \ No newline at end of file diff --git a/edxval/models.py b/edxval/models.py new file mode 100644 index 00000000..f59d135f --- /dev/null +++ b/edxval/models.py @@ -0,0 +1,80 @@ +""" +Django models for videos for Video Abstraction Layer (VAL) +""" + +from django.db import models + + +class Profile(models.Model): + """ + Details for pre-defined encoding format + """ + profile_name = models.CharField(max_length=50, unique=True) + extension = models.CharField(max_length=10) + width = models.PositiveIntegerField() + height = models.PositiveIntegerField() + + def __repr__(self): + return ( + u"Profile(profile_name={0.profile_name})" + ).format(self) + + def __unicode__(self): + return repr(self) + + +class Video(models.Model): + """ + Model for a Video group with the same content. + + A video can have multiple formats. This model is the collection of those + videos with fields that do not change across formats. + """ + edx_video_id = models.CharField(max_length=50, unique=True) + client_title = models.CharField(max_length=255, db_index=True) + duration = models.FloatField() + + def __repr__(self): + return ( + u"Video(client_title={0.client_title}, duration={0.duration})" + ).format(self) + + def __unicode__(self): + return repr(self) + + +class CourseVideos(models.Model): + """ + Model for the course_id associated with the video content. + + Every course-semester has a unique course_id. A video can be paired with multiple + course_id's but each pair is unique together. + """ + course_id = models.CharField(max_length=255) + video = models.ForeignKey(Video) + + class Meta: + unique_together = ("course_id", "video") + + +class EncodedVideo(models.Model): + """ + Video/encoding pair + """ + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + url = models.URLField(max_length=200) + file_size = models.PositiveIntegerField() + bitrate = models.PositiveIntegerField() + + profile = models.ForeignKey(Profile, related_name="+") + video = models.ForeignKey(Video, related_name="encoded_videos") + + def __repr__(self): + return ( + u"EncodedVideo(video={0.video.client_title}, " + u"profile={0.profile.profile_name})" + ).format(self) + + def __unicode__(self): + return repr(self) \ No newline at end of file diff --git a/edxval/serializers.py b/edxval/serializers.py new file mode 100644 index 00000000..675da7ed --- /dev/null +++ b/edxval/serializers.py @@ -0,0 +1,44 @@ +""" +Serializers for Video Abstraction Layer +""" +from rest_framework import serializers +from django.core.validators import MinValueValidator + +from edxval.models import Profile + + +class VideoSerializer(serializers.Serializer): + edx_video_id = serializers.CharField(required=True, max_length=50) + duration = serializers.FloatField() + client_title = serializers.CharField(max_length=255) + + +class ProfileSerializer(serializers.ModelSerializer): + class Meta: + model = Profile + fields = ( + "profile_name", + "extension", + "width", + "height" + ) + + +class OnlyEncodedVideoSerializer(serializers.Serializer): + """ + Used to serialize the EncodedVideo fir the EncodedVideoSetSerializer + """ + url = serializers.URLField(max_length=200) + file_size = serializers.IntegerField(validators=[MinValueValidator(1)]) + bitrate = serializers.IntegerField(validators=[MinValueValidator(1)]) + profile = ProfileSerializer() + + +class EncodedVideoSetSerializer(serializers.Serializer): + """ + Used to serialize a list of EncodedVideo objects it's foreign key Video Object. + """ + edx_video_id = serializers.CharField(max_length=50) + client_title = serializers.CharField(max_length=255) + duration = serializers.FloatField(validators=[MinValueValidator(1)]) + encoded_videos = OnlyEncodedVideoSerializer() diff --git a/edxval/settings.py b/edxval/settings.py new file mode 100644 index 00000000..4ffe2cea --- /dev/null +++ b/edxval/settings.py @@ -0,0 +1,167 @@ +# Django settings for edxval project. + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + # ('Your Name', 'your_email@example.com'), +) + +MANAGERS = ADMINS + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': 'video.db', # Or path to database file if using sqlite3. + 'USER': '', # Not used with sqlite3. + 'PASSWORD': '', # Not used with sqlite3. + 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '', # Set to empty string for default. Not used with sqlite3. + } +} + +# Hosts/domain names that are valid for this site; required if DEBUG is False +# See https://docs.djangoproject.com/en/1.4/ref/settings/#allowed-hosts +ALLOWED_HOSTS = [] + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# In a Windows environment this must be set to your system time zone. +TIME_ZONE = 'America/New_York' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale. +USE_L10N = True + +# If you set this to False, Django will not use timezone-aware datetimes. +USE_TZ = True + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/home/media/media.lawrence.com/media/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash. +# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" +MEDIA_URL = '' + +# Absolute path to the directory static files should be collected to. +# Don't put anything in this directory yourself; store your static files +# in apps' "static/" subdirectories and in STATICFILES_DIRS. +# Example: "/home/media/media.lawrence.com/static/" +STATIC_ROOT = '' + +# URL prefix for static files. +# Example: "http://media.lawrence.com/static/" +STATIC_URL = '/static/' + +# Additional locations of static files +STATICFILES_DIRS = ( + # Put strings here, like "/home/html/static" or "C:/www/django/static". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +# 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + +# Make this unique, and don't share it with anybody. +SECRET_KEY = ')5n@d^*763&##4c(vtzg6&%d7^yiee@5zk-n$rw7djcmz+4u4n' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + # Uncomment the next line for simple clickjacking protection: + # 'django.middleware.clickjacking.XFrameOptionsMiddleware', +) + +ROOT_URLCONF = 'edxval.urls' + +# Python dotted path to the WSGI application used by Django's runserver. +WSGI_APPLICATION = 'edxval.wsgi.application' + +TEMPLATE_DIRS = ( + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'edxval', + 'django_nose', + # Uncomment the next line to enable the admin: + 'django.contrib.admin', + # Uncomment the next line to enable admin documentation: + # 'django.contrib.admindocs', +) +TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' + +TEST_APPS = ('edxval',) + +NOSE_ARGS = [ + '--with-coverage', + '--cover-package=' + ",".join(TEST_APPS), + '--cover-branches', + '--cover-erase', +] + +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins on every HTTP 500 error when DEBUG=False. +# See http://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse' + } + }, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['require_debug_false'], + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + } +} \ No newline at end of file diff --git a/edxval/tests/__init__.py b/edxval/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/edxval/tests/constants.py b/edxval/tests/constants.py new file mode 100644 index 00000000..37e3482f --- /dev/null +++ b/edxval/tests/constants.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +EDX_VIDEO_ID = "thisis12char-thisis7" + +ENCODED_VIDEO_DICT_MOBILE = dict( + url="http://www.meowmix.com", + file_size=25556, + bitrate=9600, +) + +ENCODED_VIDEO_DICT_DESKTOP = dict( + url="http://www.meowmagic.com", + file_size=25556, + bitrate=9600, +) + +ENCODED_VIDEO_DICT_NEGATIVE_FILESIZE = dict( + url="http://www.meowmix.com", + file_size=-25556, + bitrate=9600, +) +ENCODED_VIDEO_DICT_NEGATIVE_BITRATE = dict( + url="http://www.meowmix.com", + file_size=25556, + bitrate=-9600, +) + +PROFILE_DICT_MOBILE = dict( + profile_name="mobile", + extension="avi", + width=100, + height=101 +) + +PROFILE_DICT_DESKTOP = dict( + profile_name="desktop", + extension="mp4", + width=200, + height=2001 +) +PROFILE_DICT_NON_LATIN = dict( + profile_name=u"배고파", + extension="mew", + width=100, + height=300 +) +VIDEO_DICT_CATS = dict( + client_title="Thunder Cats S01E01", + duration=111.00, + edx_video_id="thisis12char-thisis7", +) + +VIDEO_DICT_NEGATIVE_DURATION = dict( + client_title="Thunder Cats S01E01", + duration=-111, + edx_video_id="thisis12char-thisis7", +) \ No newline at end of file diff --git a/edxval/tests/test_api.py b/edxval/tests/test_api.py new file mode 100644 index 00000000..d52420df --- /dev/null +++ b/edxval/tests/test_api.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +""" +Tests for the API for Video Abstraction Layer +""" + +import mock + +from django.test import TestCase +from django.db import DatabaseError + +from edxval.models import Profile, Video, EncodedVideo +from edxval import api as api +from edxval.serializers import EncodedVideoSetSerializer +from edxval.tests import constants + + +class GetVideoInfoTest(TestCase): + + def setUp(self): + """ + Creates EncodedVideo objects in database + """ + Profile.objects.create(**constants.PROFILE_DICT_MOBILE) + Profile.objects.create(**constants.PROFILE_DICT_DESKTOP) + Video.objects.create(**constants.VIDEO_DICT_CATS) + EncodedVideo.objects.create( + video=Video.objects.get( + edx_video_id=constants.VIDEO_DICT_CATS.get("edx_video_id") + ), + profile=Profile.objects.get(profile_name="mobile"), + **constants.ENCODED_VIDEO_DICT_MOBILE + ) + EncodedVideo.objects.create( + video=Video.objects.get( + edx_video_id=constants.VIDEO_DICT_CATS.get("edx_video_id") + ), + profile=Profile.objects.get(profile_name="desktop"), + **constants.ENCODED_VIDEO_DICT_DESKTOP + ) + + def test_get_video_found(self): + """ + Tests for successful video request + """ + self.assertIsNotNone(api.get_video_info(constants.EDX_VIDEO_ID)) + + def test_no_such_video(self): + """ + Tests searching for a video that does not exist + """ + with self.assertRaises(api.ValVideoNotFoundError): + api.get_video_info("non_existing-video__") + + def test_unicode_input(self): + """ + Tests if unicode inputs are handled correctly + """ + with self.assertRaises(api.ValVideoNotFoundError): + api.get_video_info(u"๓ﻉѻฝ๓ٱซ") + + @mock.patch.object(EncodedVideoSetSerializer, '__init__') + def test_force_internal_error(self, mock_init): + """ + Tests to see if an unknown error will be handled + """ + mock_init.side_effect = Exception("Mock error") + with self.assertRaises(api.ValInternalError): + api.get_video_info(constants.EDX_VIDEO_ID) + + @mock.patch.object(Video.objects, 'get') + def test_force_database_error(self, mock_get): + """ + Tests to see if an database error will be handled + """ + mock_get.side_effect = DatabaseError("DatabaseError") + with self.assertRaises(api.ValInternalError): + api.get_video_info(constants.EDX_VIDEO_ID) \ No newline at end of file diff --git a/edxval/tests/test_models.py b/edxval/tests/test_models.py new file mode 100644 index 00000000..76864c33 --- /dev/null +++ b/edxval/tests/test_models.py @@ -0,0 +1,3 @@ +""" +Tests for Video Abstraction Layer models +""" \ No newline at end of file diff --git a/edxval/tests/test_serializers.py b/edxval/tests/test_serializers.py new file mode 100644 index 00000000..f5797733 --- /dev/null +++ b/edxval/tests/test_serializers.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +""" +Tests the serializers for the Video Abstraction Layer +""" + +from django.test import TestCase + +from edxval.serializers import ( + OnlyEncodedVideoSerializer, + EncodedVideoSetSerializer, + ProfileSerializer +) +from edxval.models import Profile +from edxval.tests import constants + + +class SerializerTests(TestCase): + """ + Tests the Serializers + """ + def setUp(self): + """ + Creates EncodedVideo objects in database + """ + Profile.objects.create(**constants.PROFILE_DICT_MOBILE) + Profile.objects.create(**constants.PROFILE_DICT_NON_LATIN) + + def test_negative_fields(self): + """ + Tests negative inputs for a serializer + + Tests negative inputs for bitrate, file_size in EncodedVideo, + and duration in Video + """ + a = OnlyEncodedVideoSerializer( + data=constants.ENCODED_VIDEO_DICT_NEGATIVE_BITRATE).errors + self.assertEqual(a.get('bitrate')[0], + u"Ensure this value is greater than or equal to 1.") + b = OnlyEncodedVideoSerializer( + data=constants.ENCODED_VIDEO_DICT_NEGATIVE_FILESIZE).errors + self.assertEqual(b.get('file_size')[0], + u"Ensure this value is greater than or equal to 1.") + c = EncodedVideoSetSerializer( + data=constants.VIDEO_DICT_NEGATIVE_DURATION).errors + self.assertEqual(c.get('duration')[0], + u"Ensure this value is greater than or equal to 1.") + + def test_unicode_inputs(self): + """ + Tests if the serializers can accept non-latin chars + """ + self.assertIsNotNone( + ProfileSerializer(Profile.objects.get(profile_name="배고파")) + ) \ No newline at end of file diff --git a/edxval/urls.py b/edxval/urls.py new file mode 100644 index 00000000..30527b93 --- /dev/null +++ b/edxval/urls.py @@ -0,0 +1,17 @@ +from django.conf.urls import patterns, include, url + +# Uncomment the next two lines to enable the admin: +from django.contrib import admin +admin.autodiscover() + +urlpatterns = patterns('', + # Examples: + # url(r'^$', 'edxval.views.home', name='home'), + # url(r'^edxval/', include('edxval.foo.urls')), + + # Uncomment the admin/doc line below to enable admin documentation: + # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), + + # Uncomment the next line to enable the admin: + url(r'^admin/', include(admin.site.urls)), +) \ No newline at end of file diff --git a/edxval/wsgi.py b/edxval/wsgi.py new file mode 100644 index 00000000..adb3240b --- /dev/null +++ b/edxval/wsgi.py @@ -0,0 +1,28 @@ +""" +WSGI config for edxval project. + +This module contains the WSGI application used by Django's development server +and any production WSGI deployments. It should expose a module-level variable +named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover +this application via the ``WSGI_APPLICATION`` setting. + +Usually you will have the standard Django WSGI application here, but it also +might make sense to replace the whole Django WSGI application with a custom one +that later delegates to the Django one. For example, you could introduce WSGI +middleware here, or combine a Django application with an application of another +framework. + +""" +import os + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "edxval.settings") + +# This application object is used by any WSGI server configured to use this +# file. This includes Django's development server, if the WSGI_APPLICATION +# setting points here. +from django.core.wsgi import get_wsgi_application +application = get_wsgi_application() + +# Apply WSGI middleware here. +# from helloworld.wsgi import HelloWorldApplication +# application = HelloWorldApplication(application) diff --git a/manage.py b/manage.py new file mode 100755 index 00000000..a3644f26 --- /dev/null +++ b/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "edxval.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..dd67320e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +django>=1.4,<1.5 +djangorestframework==2.3.5 \ No newline at end of file diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 00000000..390f5bc6 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,3 @@ +django-nose==1.2 +coverage==3.7.1 +mock==1.0.1 \ No newline at end of file