├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.markdown ├── django_schemata ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── manage_schemata.py │ │ ├── migrate_schemata.py │ │ └── sync_schemata.py ├── middleware.py ├── models.py ├── oracle_backend │ ├── __init__.py │ └── base.py ├── postgresql_backend │ ├── __init__.py │ └── base.py └── tests.py ├── setup.py └── version.py /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | .DS_Store 4 | VERSION 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011 by Vlada Macek 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | 2 | include LICENSE 3 | include README.markdown 4 | include *.txt 5 | 6 | include version.py 7 | include VERSION -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | django-schemata 2 | =============== 3 | 4 | **BEWARE! THIS IS AN EXPERIMENTAL CODE! Created this as a proof of concept 5 | and never had a chance to test it thoroughly, not speaking about the 6 | production run, as our team changed the plans. While I was very excited 7 | during coding it, I unfortunately have no use for schemata currently. I'd 8 | love to hear how the code is really doing and if you find something that 9 | should be fixed, I'll gladly review and pull your patches.** 10 | 11 | This project adds the [PostgreSQL schema](http://www.postgresql.org/docs/8.4/static/ddl-schemas.html) 12 | support to [Django](http://www.djangoproject.com/). The schema, which can 13 | be seen as a namespace in which any database object exists, allows to isolate 14 | the database objects even when they have the same names. You can have same set 15 | of tables, indices, sequences etc. many times under the single database. 16 | 17 | In case you're not using schemata, your objects lie in the default schema 18 | `public` and because the default `search_path` contains `public`, 19 | you don't have to care. 20 | 21 | Why to care? 22 | ------------ 23 | 24 | It's simple: 25 | 26 | * One code 27 | * One instance 28 | * One shared buffering 29 | * One connection 30 | * One database 31 | * One schema for one customer 32 | * You scale up to the stars 33 | 34 | Using schemata can be very useful if you run the Software as a service (SaaS) 35 | server for multiple customers. Typically for *multiple* databases you had *single* 36 | project code, cloned many times and that required strong maintenance effort. 37 | 38 | So until recently you were forced to maintain multiple Django instances even 39 | when the code did the same things, only the data varied. With the invention 40 | of multiple databases support in Django it was possible to use it for SaaS, 41 | yet using schemata was found to bring even more advantages. 42 | 43 | This code was inspired by the [A better way : SaaS with Django and PostgreSQL Schemas](http://tidbids.posterous.com/saas-with-django-and-postgresql-schemas) 44 | blog post and the [django-appschema](https://bitbucket.org/cedarlab/django-appschema/src) 45 | application. 46 | 47 | Going underneath 48 | ---------------- 49 | 50 | Like `django-appschema` this project infers the proper schema to switch to 51 | from the hostname found in each web request. You're expected to point 52 | multiple HTTP domains of your customers handled by your (Apache/WSGI) server 53 | to the single Django instance supporting schemata. 54 | 55 | **Warning:** This application was **not tested in the multithreading** 56 | environment, we configure our mod_wsgi to run each Django instance 57 | as mutiple separated processes. 58 | 59 | Unlike `django-appschema`, this project seeks for the **maximum simplicity** 60 | (added layer and toolset must be as thin as possible so the data path is clear): 61 | 62 | * Minimalistic code. 63 | * **No hacking** of `INSTALLED_APPS`, `syncdb` or `migrate` commands... 64 | (they had enough with [South](http://south.aeracode.org/)). 65 | * Schema definitions are not stored in the database, but in `settings`'s dict. 66 | That allows you to flexibly and uniformly configure the differences between 67 | individual domains. `django-schemata` only requires `schema_name` sub-key, 68 | but you're free to store additional configuration there. 69 | 70 | Shared applications 71 | ------------------- 72 | 73 | Not yet. 74 | 75 | The reason why `django-appschema` became hackish is that it tries to 76 | sync/migrate both isolated and shared applications in a single run. The app is 77 | *shared* if it has its tables in the `public` schema, hence they're accessible 78 | by every domain. That's because `public` schema is always checked after the 79 | object was not found in its "home" schema. 80 | 81 | The support for shared application will be added to `django-schemata` as soon 82 | as it becomes clear it is required. And we strive to add the support 83 | in a more simple way: `ALTER TABLE table SET SCHEMA schema` looks 84 | *very promising*. We believe it's bearable for the admin to do some extra 85 | setup steps, when the code stays simple. 86 | 87 | Setup 88 | ----- 89 | 90 | `django-schemata` requires the following `settings.py` modifications: 91 | 92 | # We wrap around the PostgreSQL backend. 93 | DATABASE_ENGINE = 'django_schemata.postgresql_backend' 94 | 95 | # Schema switching upon web requests. 96 | MIDDLEWARE_CLASSES = ( 97 | 'django_schemata.middleware.SchemataMiddleware', 98 | ... 99 | ) 100 | 101 | # We also offer some management commands. 102 | INSTALLED_APPS = ( 103 | ... 104 | 'django_schemata', 105 | ... 106 | ) 107 | 108 | # We need to assure South of the real db backends for all databases. 109 | # Otherwise it dies in uncertainty. 110 | # For Django 1.1 or below: 111 | #SOUTH_DATABASE_ADAPTER = 'south.db.postgresql_psycopg2' 112 | # For Django 1.2 or above: 113 | SOUTH_DATABASE_ADAPTERS = { 114 | 'default': 'south.db.postgresql_psycopg2', 115 | } 116 | 117 | # This maps all HTTP domains to all schemata we want to support. 118 | # All of your supported customers need to be registered here. 119 | SCHEMATA_DOMAINS = { 120 | 'localhost': { 121 | 'schema_name': 'localhost', 122 | 'additional_data': ... 123 | }, 124 | 'first-client.com': { 125 | 'schema_name': 'firstclient', 126 | }, 127 | 'second-client.com': { 128 | 'schema_name': 'secondclient', 129 | }, 130 | } 131 | 132 | Management commands 133 | ------------------- 134 | 135 | ### ./manage.py manage_schemata ### 136 | 137 | As soon as you add your first domain to `settings.SCHEMATA_DOMAINS`, you can 138 | run this. PostgreSQL database is inspected and yet-not-existing schemata 139 | are added. Current ones are not touched (command is safe to re-run). 140 | 141 | Later more capabilities will be added here. 142 | 143 | ### ./manage.py sync_schemata ### 144 | 145 | This command runs the `syncdb` command for every registered database schema. 146 | You can sync **all** of your apps and domains in a single run. 147 | 148 | The options given to `sync_schemata` are passed to every `syncdb`. So if you 149 | use South, you may find this handy: 150 | 151 | ./manage sync_schemata --migrate 152 | 153 | ### ./manage.py migrate_schemata ### 154 | 155 | This command runs the South's `migrate` command for every registered database 156 | schema. 157 | 158 | The options given to `migrate_schemata` are passed to every `migrate`. Hence 159 | you may find 160 | 161 | ./manage.py migrate_schemata --list 162 | 163 | handy if you're curious or 164 | 165 | ./manage.py migrate_schemata myapp 0001_initial --fake 166 | 167 | in case you're just switching `myapp` application to use South migrations. 168 | 169 | Bug report? Idea? Patch? 170 | ------------------------ 171 | 172 | We're happy to incorporate your patches and ideas. Please either fork and send 173 | pull requests or just send the patch. 174 | 175 | Discuss this project! Please report bugs. 176 | 177 | Success stories are highly welcome. 178 | 179 | Thank you. 180 | -------------------------------------------------------------------------------- /django_schemata/__init__.py: -------------------------------------------------------------------------------- 1 | """This project adds the PostgreSQL schema support to Django. The schema, which can be seen as a namespace in which any database object exists, allows to isolate the database objects even when they have the same names. You can have same set of tables, indices, sequences etc. many times under the single database.""" -------------------------------------------------------------------------------- /django_schemata/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuttle/django-schemata/902406bc5053f4ef215740c8762b53f866a5ed81/django_schemata/management/__init__.py -------------------------------------------------------------------------------- /django_schemata/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuttle/django-schemata/902406bc5053f4ef215740c8762b53f866a5ed81/django_schemata/management/commands/__init__.py -------------------------------------------------------------------------------- /django_schemata/management/commands/manage_schemata.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.management.base import BaseCommand 3 | from django.db import connection, transaction 4 | 5 | from django_schemata.postgresql_backend.base import _check_identifier 6 | 7 | class Command(BaseCommand): 8 | help = "Manages the postgresql schemata." 9 | 10 | def handle(self, *unused_args, **unused_options): 11 | self.create_schemata() 12 | 13 | def create_schemata(self): 14 | """ 15 | Go through settings.SCHEMATA_DOMAINS and create all schemata that 16 | do not already exist in the database. 17 | """ 18 | # operate in the public schema 19 | connection.set_schemata_off() 20 | cursor = connection.cursor() 21 | cursor.execute('SELECT schema_name FROM information_schema.schemata') 22 | existing_schemata = [ row[0] for row in cursor.fetchall() ] 23 | 24 | for sd in settings.SCHEMATA_DOMAINS.values(): 25 | schema_name = str(sd['schema_name']) 26 | _check_identifier(schema_name) 27 | 28 | if schema_name not in existing_schemata: 29 | sql = 'CREATE SCHEMA %s' % schema_name 30 | print sql 31 | cursor.execute(sql) 32 | transaction.commit_unless_managed() 33 | -------------------------------------------------------------------------------- /django_schemata/management/commands/migrate_schemata.py: -------------------------------------------------------------------------------- 1 | from django_schemata.management.commands.sync_schemata import BaseSchemataCommand 2 | 3 | # Uses the twin command base code for the actual iteration. 4 | 5 | class Command(BaseSchemataCommand): 6 | COMMAND_NAME = 'migrate' 7 | -------------------------------------------------------------------------------- /django_schemata/management/commands/sync_schemata.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.management import call_command, get_commands, load_command_class 3 | from django.core.management.base import BaseCommand 4 | from django.db import connection 5 | 6 | class BaseSchemataCommand(BaseCommand): 7 | """ 8 | Generic command class useful for iterating any existing command 9 | over all schemata. The actual command name is expected in the 10 | class variable COMMAND_NAME of the subclass. 11 | """ 12 | def __new__(cls, *args, **kwargs): 13 | """ 14 | Sets option_list and help dynamically. 15 | """ 16 | # instantiate 17 | obj = super(BaseSchemataCommand, cls).__new__(cls, *args, **kwargs) 18 | # load the command class 19 | cmdclass = load_command_class(get_commands()[obj.COMMAND_NAME], obj.COMMAND_NAME) 20 | # inherit the options from the original command 21 | obj.option_list = cmdclass.option_list 22 | # prepend the command's original help with the info about schemata iteration 23 | obj.help = "Calls %s for all registered schemata. You can use regular %s options. " \ 24 | "Original help for %s: %s" \ 25 | % (obj.COMMAND_NAME, obj.COMMAND_NAME, obj.COMMAND_NAME, \ 26 | getattr(cmdclass, 'help', 'none')) 27 | return obj 28 | 29 | def handle(self, *args, **options): 30 | """ 31 | Iterates a command over all registered schemata. 32 | """ 33 | for domain_name in settings.SCHEMATA_DOMAINS: 34 | 35 | print 36 | print self.style.NOTICE("=== Switching to domain ") \ 37 | + self.style.SQL_TABLE(domain_name) \ 38 | + self.style.NOTICE(" then calling %s:" % self.COMMAND_NAME) 39 | 40 | # sets the schema for the connection 41 | connection.set_schemata_domain(domain_name) 42 | 43 | # call the original command with the args it knows 44 | call_command(self.COMMAND_NAME, *args, **options) 45 | 46 | 47 | class Command(BaseSchemataCommand): 48 | COMMAND_NAME = 'syncdb' 49 | -------------------------------------------------------------------------------- /django_schemata/middleware.py: -------------------------------------------------------------------------------- 1 | from django.db import connection 2 | 3 | class SchemataMiddleware(object): 4 | """ 5 | This middleware should be placed at the very top of the middleware stack. 6 | Selects the proper database schema using the request host. Can fail in 7 | various ways which is better than corrupting or revealing data... 8 | """ 9 | def process_request(self, request): 10 | hostname_without_port = request.get_host().split(':')[0] 11 | request.schema_domain_name = hostname_without_port 12 | request.schema_domain = connection.set_schemata_domain(request.schema_domain_name) 13 | 14 | # The question remains whether it's necessary to unset the schema 15 | # when the request finishes... 16 | -------------------------------------------------------------------------------- /django_schemata/models.py: -------------------------------------------------------------------------------- 1 | # Placeholder to allow tests to be found -------------------------------------------------------------------------------- /django_schemata/oracle_backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuttle/django-schemata/902406bc5053f4ef215740c8762b53f866a5ed81/django_schemata/oracle_backend/__init__.py -------------------------------------------------------------------------------- /django_schemata/oracle_backend/base.py: -------------------------------------------------------------------------------- 1 | import os, re 2 | 3 | from django.conf import settings 4 | from django.utils.importlib import import_module 5 | from django.core.exceptions import ImproperlyConfigured 6 | 7 | ORIGINAL_BACKEND = getattr(settings, 'ORIGINAL_BACKEND', 'django.db.backends.oracle') 8 | 9 | original_backend = import_module('.base', ORIGINAL_BACKEND) 10 | 11 | SQL_IDENTIFIER_RE = re.compile('^[_a-zA-Z][_a-zA-Z0-9]{,30}$') 12 | 13 | def _check_identifier(identifier): 14 | if not SQL_IDENTIFIER_RE.match(identifier): 15 | raise RuntimeError("Invalid string used for the schema name.") 16 | 17 | class DatabaseWrapper(original_backend.DatabaseWrapper): 18 | def __init__(self, *args, **kwargs): 19 | super(DatabaseWrapper, self).__init__(*args, **kwargs) 20 | 21 | # By default the schema is not set 22 | self.schema_name = None 23 | 24 | # but one can change the default using the environment variable. 25 | force_domain = os.getenv('DJANGO_SCHEMATA_DOMAIN') 26 | if force_domain: 27 | self.schema_name = self._resolve_schema_domain(force_domain)['schema_name'] 28 | 29 | def _resolve_schema_domain(self, domain_name): 30 | try: 31 | sd = settings.SCHEMATA_DOMAINS[domain_name] 32 | except KeyError, er: 33 | print er 34 | raise ImproperlyConfigured("Domain '%s' is not supported by " 35 | "settings.SCHEMATA_DOMAINS" % domain_name) 36 | return sd 37 | 38 | def _set_oracle_default_schema(self, cursor): 39 | ''' 40 | this is somewhat the equivalent of postgresql_backend ``_set_pg_search_path`` 41 | 42 | .. note:: 43 | 44 | ORACLE does not allow a fallback to the current USER schema like in 45 | PostgreSQL with the ``public`` schema 46 | ''' 47 | if self.schema_name is None: 48 | if settings.DEBUG: 49 | full_info = " Choices are: %s." \ 50 | % ', '.join(settings.SCHEMATA_DOMAINS.keys()) 51 | else: 52 | full_info = "" 53 | raise ImproperlyConfigured("Database schema not set (you can pick " 54 | "one of the supported domains by setting " 55 | "then DJANGO_SCHEMATA_DOMAIN environment " 56 | "variable.%s)" % full_info) 57 | 58 | base_sql_command = 'ALTER SESSION SET current_schema = ' 59 | 60 | if self.schema_name == '': 61 | # set the current_schema to a current USER 62 | cursor.execute("""begin 63 | EXECUTE IMMEDIATE '%s' || USER; 64 | end; 65 | /""" % base_sql_command) 66 | else: 67 | _check_identifier(self.schema_name) 68 | sql_command = base_sql_command + self.schema_name 69 | cursor.execute(sql_command) 70 | 71 | def set_schemata_domain(self, domain_name): 72 | """ 73 | Main API method to current database schema, 74 | but it does not actually modify the db connection. 75 | Returns the particular domain dict from settings.SCHEMATA_DOMAINS. 76 | """ 77 | sd = self._resolve_schema_domain(domain_name) 78 | self.schema_name = sd['schema_name'] 79 | return sd 80 | 81 | def set_schemata_off(self): 82 | """ 83 | Instructs to stay in the common 'public' schema. 84 | """ 85 | self.schema_name = '' 86 | 87 | def _cursor(self): 88 | """ 89 | Here it happens. We hope every Django db operation using Oracle 90 | must go through this to get the cursor handle. 91 | """ 92 | cursor = super(DatabaseWrapper, self)._cursor() 93 | self._set_oracle_default_schema(cursor) 94 | return cursor 95 | 96 | DatabaseError = original_backend.DatabaseError 97 | IntegrityError = original_backend.IntegrityError 98 | -------------------------------------------------------------------------------- /django_schemata/postgresql_backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuttle/django-schemata/902406bc5053f4ef215740c8762b53f866a5ed81/django_schemata/postgresql_backend/__init__.py -------------------------------------------------------------------------------- /django_schemata/postgresql_backend/base.py: -------------------------------------------------------------------------------- 1 | import os, re 2 | 3 | from django.conf import settings 4 | from django.utils.importlib import import_module 5 | from django.core.exceptions import ImproperlyConfigured 6 | 7 | ORIGINAL_BACKEND = getattr(settings, 'ORIGINAL_BACKEND', 'django.db.backends.postgresql_psycopg2') 8 | 9 | original_backend = import_module('.base', ORIGINAL_BACKEND) 10 | 11 | # from the postgresql doc 12 | SQL_IDENTIFIER_RE = re.compile('^[_a-zA-Z][_a-zA-Z0-9]{,62}$') 13 | 14 | def _check_identifier(identifier): 15 | if not SQL_IDENTIFIER_RE.match(identifier): 16 | raise RuntimeError("Invalid string used for the schema name.") 17 | 18 | class DatabaseWrapper(original_backend.DatabaseWrapper): 19 | def __init__(self, *args, **kwargs): 20 | super(DatabaseWrapper, self).__init__(*args, **kwargs) 21 | 22 | # By default the schema is not set 23 | self.schema_name = None 24 | 25 | # but one can change the default using the environment variable. 26 | force_domain = os.getenv('DJANGO_SCHEMATA_DOMAIN') 27 | if force_domain: 28 | self.schema_name = self._resolve_schema_domain(force_domain)['schema_name'] 29 | 30 | def _resolve_schema_domain(self, domain_name): 31 | try: 32 | sd = settings.SCHEMATA_DOMAINS[domain_name] 33 | except KeyError: 34 | raise ImproperlyConfigured("Domain '%s' is not supported by " 35 | "settings.SCHEMATA_DOMAINS" % domain_name) 36 | return sd 37 | 38 | def _set_pg_search_path(self, cursor): 39 | """ 40 | Actual search_path modification for the cursor. Database will 41 | search schemata from left to right when looking for the object 42 | (table, index, sequence, etc.). 43 | """ 44 | if self.schema_name is None: 45 | if settings.DEBUG: 46 | full_info = " Choices are: %s." \ 47 | % ', '.join(settings.SCHEMATA_DOMAINS.keys()) 48 | else: 49 | full_info = "" 50 | raise ImproperlyConfigured("Database schema not set (you can pick " 51 | "one of the supported domains by setting " 52 | "then DJANGO_SCHEMATA_DOMAIN environment " 53 | "variable.%s)" % full_info) 54 | 55 | _check_identifier(self.schema_name) 56 | if self.schema_name == 'public': 57 | cursor.execute('SET search_path = public') 58 | else: 59 | cursor.execute('SET search_path = %s, public', [self.schema_name]) 60 | 61 | def set_schemata_domain(self, domain_name): 62 | """ 63 | Main API method to current database schema, 64 | but it does not actually modify the db connection. 65 | Returns the particular domain dict from settings.SCHEMATA_DOMAINS. 66 | """ 67 | sd = self._resolve_schema_domain(domain_name) 68 | self.schema_name = sd['schema_name'] 69 | return sd 70 | 71 | def set_schemata_off(self): 72 | """ 73 | Instructs to stay in the common 'public' schema. 74 | """ 75 | self.schema_name = 'public' 76 | 77 | def _cursor(self): 78 | """ 79 | Here it happens. We hope every Django db operation using PostgreSQL 80 | must go through this to get the cursor handle. We change the path. 81 | """ 82 | cursor = super(DatabaseWrapper, self)._cursor() 83 | self._set_pg_search_path(cursor) 84 | return cursor 85 | 86 | DatabaseError = original_backend.DatabaseError 87 | IntegrityError = original_backend.IntegrityError 88 | -------------------------------------------------------------------------------- /django_schemata/tests.py: -------------------------------------------------------------------------------- 1 | from django import test 2 | from django.db import connection 3 | from django.core.exceptions import ImproperlyConfigured 4 | from django_schemata.postgresql_backend.base import DatabaseError 5 | from django.db.utils import DatabaseError 6 | from django.conf import settings 7 | from django.core.management import call_command 8 | from django.contrib.sites.models import Site 9 | 10 | # only run this test if the custom database wrapper is in use. 11 | if hasattr(connection, 'schema_name'): 12 | 13 | # This will fail with Django==1.3.1 AND psycopg2==2.4.2 14 | # See https://code.djangoproject.com/ticket/16250 15 | # Either upgrade Django to trunk or use psycopg2==2.4.1 16 | connection.set_schemata_off() 17 | 18 | 19 | def set_schematas(domain): 20 | settings.SCHEMATA_DOMAINS = { 21 | domain: { 22 | 'schema_name': domain, 23 | } 24 | } 25 | 26 | 27 | def add_schemata(domain): 28 | settings.SCHEMATA_DOMAINS.update({ 29 | domain: { 30 | 'schema_name': domain, 31 | } 32 | }) 33 | 34 | 35 | class SchemataTestCase(test.TestCase): 36 | def setUp(self): 37 | set_schematas('blank') 38 | self.c = test.client.Client() 39 | 40 | def tearDown(self): 41 | connection.set_schemata_off() 42 | 43 | def test_unconfigured_domain(self): 44 | self.assertRaises(ImproperlyConfigured, self.c.get, '/') 45 | 46 | def test_unmanaged_domain(self): 47 | add_schemata('not_in_db') 48 | self.assertRaises(DatabaseError, self.c.get, '/', HTTP_HOST='not_in_db') 49 | 50 | def test_domain_switch(self): 51 | add_schemata('test1') 52 | add_schemata('test2') 53 | call_command('manage_schemata') 54 | 55 | self.c.get('/', HTTP_HOST='test1') 56 | test1 = Site.objects.get(id=1) 57 | test1.domain = 'test1' 58 | test1.save() 59 | 60 | self.c.get('/', HTTP_HOST='test2') 61 | test2 = Site.objects.get(id=1) 62 | test2.domain = 'test2' 63 | test2.save() 64 | 65 | self.c.get('/', HTTP_HOST='test1') 66 | test = Site.objects.get_current() 67 | self.assertEqual(test.domain, 'test1', 'Current site should be "test1", not "%s"' % test.domain) 68 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | from version import get_git_version 4 | 5 | def read_file(filename): 6 | """Read a file into a string""" 7 | path = os.path.abspath(os.path.dirname(__file__)) 8 | filepath = os.path.join(path, filename) 9 | try: 10 | return open(filepath).read() 11 | except IOError: 12 | return '' 13 | 14 | # Use the docstring of the __init__ file to be the description 15 | DESC = " ".join(__import__('django_schemata').__doc__.splitlines()).strip() 16 | 17 | setup( 18 | name = "django-schemata", 19 | version = get_git_version(), 20 | url = 'https://github.com/tuttle/django-schemata', 21 | author = 'Vlada Macek', 22 | author_email = 'macek@sandbox.cz', 23 | description = DESC, 24 | long_description = read_file('README'), 25 | packages = find_packages(), 26 | include_package_data = True, 27 | install_requires=read_file('requirements.txt'), 28 | classifiers = [ 29 | 'License :: OSI Approved :: MIT License', 30 | 'Framework :: Django', 31 | ], 32 | ) 33 | -------------------------------------------------------------------------------- /version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Author: Douglas Creager 3 | # This file is placed into the public domain. 4 | 5 | # Calculates the current version number. If possible, this is the 6 | # output of “git describe”, modified to conform to the versioning 7 | # scheme that setuptools uses. If “git describe” returns an error 8 | # (most likely because we're in an unpacked copy of a release tarball, 9 | # rather than in a git working copy), then we fall back on reading the 10 | # contents of the VERSION file. 11 | # 12 | # To use this script, simply import it your setup.py file, and use the 13 | # results of get_git_version() as your package version: 14 | # 15 | # from version import * 16 | # 17 | # setup( 18 | # version=get_git_version(), 19 | # . 20 | # . 21 | # . 22 | # ) 23 | # 24 | # This will automatically update the VERSION file, if 25 | # necessary. Note that the VERSION file should *not* be 26 | # checked into git; please add it to your top-level .gitignore file. 27 | # 28 | # You'll probably want to distribute the VERSION file in your 29 | # sdist tarballs; to do this, just create a MANIFEST.in file that 30 | # contains the following line: 31 | # 32 | # include VERSION 33 | 34 | __all__ = ("get_git_version") 35 | 36 | from subprocess import Popen, PIPE 37 | 38 | 39 | def call_git_describe(): 40 | try: 41 | p = Popen(['git', 'describe', '--tags', '--always'], 42 | stdout=PIPE, stderr=PIPE) 43 | p.stderr.close() 44 | line = p.stdout.readlines()[0] 45 | return line.strip() 46 | 47 | except: 48 | return None 49 | 50 | 51 | def read_release_version(): 52 | try: 53 | f = open("VERSION", "r") 54 | 55 | try: 56 | version = f.readlines()[0] 57 | return version.strip() 58 | 59 | finally: 60 | f.close() 61 | 62 | except: 63 | return None 64 | 65 | 66 | def write_release_version(version): 67 | f = open("VERSION", "w") 68 | f.write("%s\n" % version) 69 | f.close() 70 | 71 | 72 | def get_git_version(): 73 | # Read in the version that's currently in VERSION. 74 | 75 | release_version = read_release_version() 76 | 77 | # First try to get the current version using “git describe”. 78 | 79 | version = call_git_describe() 80 | 81 | # If that doesn't work, fall back on the value that's in 82 | # VERSION. 83 | 84 | if version is None: 85 | version = release_version 86 | 87 | # If we still don't have anything, that's an error. 88 | 89 | if version is None: 90 | raise ValueError("Cannot find the version number!") 91 | 92 | # If the current version is different from what's in the 93 | # VERSION file, update the file to be current. 94 | 95 | if version != release_version: 96 | write_release_version(version) 97 | 98 | # Finally, return the current version. 99 | 100 | return version 101 | 102 | 103 | if __name__ == "__main__": 104 | print get_git_version() 105 | --------------------------------------------------------------------------------