├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── dbview ├── __init__.py ├── helpers.py └── models.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # ingore file created by pycharm 60 | .idea 61 | 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Manuel Francisco Naranjo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | #################### 2 | django-database-view 3 | #################### 4 | 5 | A simple pluggable application that allows to work with database views. 6 | 7 | Quick start 8 | =========== 9 | 10 | 1. Install the package:: 11 | 12 | pip install django-database-view 13 | 14 | 2. In your ``models.py`` create classes which extend ``dbview.models.DbView`` 15 | like this: 16 | 17 | .. code-block:: python 18 | 19 | from django.db import models 20 | from dbview.models import DbView 21 | 22 | class ModelA(models.Model): 23 | fielda = models.CharField(max_length=64) 24 | fieldc = models.IntegerField() 25 | 26 | class MyView(DbView): 27 | fieldA = models.OneToOneField(ModelA, primary_key=True, 28 | on_delete=models.DO_NOTHING, db_column='fielda__id') 29 | fieldB = models.IntegerField(blank=True, null=True, db_column='fieldb') 30 | 31 | @classmethod 32 | def view(cls): 33 | """ 34 | This method returns the SQL string that creates the view, 35 | in this example fieldB is the result of annotating another column 36 | """ 37 | qs = modelA.objects.all( 38 | ).annotate( 39 | fieldb=models.Sum('fieldc'), 40 | ).annotate( 41 | fielda__id=models.F('pk'), 42 | ).order_by( 43 | 'fielda__id', 44 | ).values( 45 | 'fielda__id', 46 | 'fieldb', 47 | ) 48 | return str(qs.query) 49 | 50 | Alternatively ``get_view_str`` method could be used to write a custom SQL: 51 | 52 | .. code-block:: python 53 | 54 | class MyView(DbView): 55 | # ... 56 | 57 | @classmethod 58 | def get_view_str(cls): 59 | return """ 60 | CREATE VIEW my_view AS ( 61 | SELECT ... 62 | )""" 63 | 64 | 3. Then create a migration point for your view generation, edit that 65 | migration and modify it, add: 66 | ``from dbview.helpers import CreateView`` and replace the line the 67 | call to ``migrations.CreateModel`` with ``CreateView``. 68 | 69 | 4. Migrate your database and start using your database views. 70 | -------------------------------------------------------------------------------- /dbview/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manuelnaranjo/django-database-view/42ceb2cac24e2c404443a3835f1d835620fc6030/dbview/__init__.py -------------------------------------------------------------------------------- /dbview/helpers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.db import migrations 4 | from django.apps import apps 5 | 6 | 7 | class CreateView(migrations.CreateModel): 8 | 9 | def database_forwards(self, app_label, schema_editor, from_state, to_state): 10 | fake_model = to_state.apps.get_model(app_label, self.name) 11 | 12 | if not self.allow_migrate_model( 13 | schema_editor.connection.alias, fake_model): 14 | return 15 | 16 | model = self._get_model(fake_model, app_label, to_state) 17 | 18 | self._drop_view(fake_model, model, schema_editor) 19 | 20 | if hasattr(model, 'view'): 21 | self._create_standard_view(model, schema_editor) 22 | elif hasattr(model, 'get_view_str'): 23 | self._create_view_from_raw_sql(model.get_view_str(), schema_editor) 24 | else: 25 | raise Exception('{} has neither view nor get_view_str'.format( 26 | model)) 27 | 28 | def database_backwards(self, app_label, schema_editor, from_state, to): 29 | fake_model = from_state.apps.get_model(app_label, self.name) 30 | model = self._get_model(fake_model, app_label, to) 31 | self._drop_view(fake_model, model, schema_editor) 32 | 33 | def _get_model(self, state, app_label, fake_model): 34 | models = apps.get_app_config(app_label).models_module 35 | 36 | if hasattr(models, self.name): 37 | return getattr(models, self.name) 38 | 39 | # TODO: identify model more reliably and support more than 1 level 40 | for submodule in models.__dict__.values(): 41 | if hasattr(submodule, self.name): 42 | return getattr(submodule, self.name) 43 | 44 | logging.warning('Using fake model, this may fail with inherited views') 45 | return fake_model 46 | 47 | def _drop_view(self, fake_model, model, schema_editor): 48 | if hasattr(model, 'drop_view_sql'): 49 | sql_template = model.drop_view_sql 50 | else: 51 | sql_template = 'DROP VIEW IF EXISTS %(table)s' 52 | args = { 53 | 'table': schema_editor.quote_name(fake_model._meta.db_table), 54 | } 55 | sql = sql_template % args 56 | schema_editor.execute(sql, None) 57 | 58 | def _create_standard_view(self, model, schema_editor): 59 | sql_template = 'CREATE VIEW %(table)s AS %(definition)s' 60 | qs = str(model.view()) 61 | args = { 62 | 'table': schema_editor.quote_name(model._meta.db_table), 63 | 'definition': qs, 64 | } 65 | sql = sql_template % args 66 | self._create_view_from_raw_sql(sql, schema_editor) 67 | 68 | def _create_view_from_raw_sql(self, sql, schema_editor): 69 | schema_editor.execute(sql, None) 70 | -------------------------------------------------------------------------------- /dbview/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class ViewManager(models.Manager): 5 | def bulk_create(self, *args, **kwargs): 6 | raise NotImplementedError 7 | 8 | def create(self, *args, **kwargs): 9 | raise NotImplementedError 10 | 11 | def get_or_create(self, *args, **kwargs): 12 | raise NotImplementedError 13 | 14 | def delete(self, *args, **kwargs): 15 | raise NotImplementedError 16 | 17 | def update(self, *args, **kwargs): 18 | raise NotImplementedError 19 | 20 | 21 | class DbView(models.Model): 22 | objects = ViewManager() 23 | 24 | class Meta: 25 | abstract = True 26 | 27 | def delete(self, *args, **kwargs): 28 | raise NotImplementedError 29 | 30 | def save(self, *args, **kwargs): 31 | raise NotImplementedError 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | 5 | def read(fname): 6 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 7 | 8 | 9 | setup( 10 | name='django-database-view', 11 | version='0.3.0', 12 | packages=['dbview'], 13 | long_description=read('README.rst'), 14 | include_package_data=True, 15 | license='MIT', 16 | description='A simple Django app to handle database views.', 17 | url='https://github.com/manuelnaranjo/django-database-view', 18 | author='Manuel F. Naranjo', 19 | author_email='naranjo.manuel@gmail.com', 20 | classifiers=[ 21 | 'Environment :: Web Environment', 22 | 'Framework :: Django', 23 | 'Intended Audience :: Developers', 24 | 'License :: OSI Approved :: MIT License', 25 | 'Operating System :: OS Independent', 26 | 'Programming Language :: Python', 27 | 'Programming Language :: Python :: 2', 28 | 'Programming Language :: Python :: 2.7', 29 | 'Programming Language :: Python :: 3', 30 | 'Programming Language :: Python :: 3.2', 31 | 'Programming Language :: Python :: 3.3', 32 | 'Programming Language :: Python :: 3.4', 33 | 'Programming Language :: Python :: 3.5', 34 | 'Programming Language :: Python :: 3.6', 35 | 'Programming Language :: Python :: 3.7', 36 | 'Programming Language :: Python :: 3.8', 37 | 'Topic :: Internet :: WWW/HTTP', 38 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 39 | ], 40 | ) 41 | --------------------------------------------------------------------------------