├── AUTHORS ├── CHANGELOG.textile ├── MANIFEST.in ├── README.rst ├── __pkginfo__.py ├── django_redis_engine ├── __init__.py ├── base.py ├── client.py ├── compiler.py ├── creation.py ├── index_utils.py ├── redis_entity.py ├── redis_transaction.py └── serializer.py ├── normalize_whitespace ├── setup.py └── testproject ├── __init__.py ├── dbindexes.py ├── manage.py ├── settings.py ├── templates ├── 404.html ├── 500.html ├── aproposito.html ├── base.html ├── come_aiutare.html ├── home.html ├── login.html ├── menu.html ├── pagination.html └── registration │ └── login.html ├── testapp ├── __init__.py ├── dbindexes.py ├── forms.py ├── models.py ├── redis_settings.py ├── templates │ └── testapp │ │ ├── .html │ │ ├── add_answer.html │ │ ├── add_post.html │ │ ├── forms │ │ ├── AnswerForm.html │ │ └── MyForm.html │ │ └── posts.html ├── tests.py ├── urls.py └── views.py └── urls.py /AUTHORS: -------------------------------------------------------------------------------- 1 | Mirko Rossini 2 | 3 | -------------------------------------------------------------------------------- /CHANGELOG.textile: -------------------------------------------------------------------------------- 1 | 23-04-2011 2 | 3 | Added tests for (i)endswith 4 | 5 | 6 | General optimization with pipelines and persistent connections 7 | 8 | 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include README.rst 3 | include AUTHORS 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========================================== 2 | Django Redis Engine for django-nonrel 0.1 3 | ========================================== 4 | 5 | Requirements 6 | ============ 7 | * Redis 8 | * Redis bindings for python 9 | * `Django-nonrel`_ 10 | * `djangotoolbox`_ 11 | .. _Django-nonrel: http://bitbucket.org/wkornewald/django-nonrel 12 | .. _djangotoolbox: http://bitbucket.org/wkornedwald/djangotoolbox 13 | 14 | Optional Requirements 15 | ============ 16 | * dbindexer 17 | 18 | Features 19 | ======== 20 | Indexing for: 21 | * startswith,istartswith 22 | * endswith,iendswith 23 | * gt,gte,lt,lte 24 | * contains (using dbindexer) 25 | 26 | Redis transaction support: you can execute multiple insert of django objects in one single pipeline. See testproject/testapp/tests.py 27 | Count queries 28 | 29 | 30 | Missing features 31 | =========== 32 | Aggregate queries 33 | Documentation (although testproject is self-documented) 34 | 35 | 36 | 37 | Contributing 38 | ============ 39 | You are highly encouraged to participate in the development, simply use 40 | GitHub's fork/pull request system. 41 | If you don't like GitHub (for some reason) you're welcome 42 | to send regular patches to the mailing list. 43 | -------------------------------------------------------------------------------- /__pkginfo__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import codecs 4 | import django_redis_engine as distmeta 5 | 6 | distname = 'django_redis_engine' 7 | numversion = distmeta.__version__ 8 | version = '.'.join(map(str, numversion)) 9 | license = '2-clause BSD' 10 | author = distmeta.__author__ 11 | author_email = distmeta.__contact__ 12 | 13 | short_desc = "A Redis django db backend." 14 | long_desc = codecs.open('README.rst', 'r', 'utf-8').read() 15 | 16 | install_requires = [ 'django>=1.2', 'djangotoolbox'] 17 | pyversions = ['2', '2.4', '2.5', '2.6', '2.7'] 18 | docformat = distmeta.__docformat__ 19 | include_dirs = [] 20 | -------------------------------------------------------------------------------- /django_redis_engine/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | __version__ = (0, 1, 0) 5 | __author__ = "Mirko Rossini" 6 | __contact__ = "isaidyep@gmail.com" 7 | __docformat__ = "restructuredtext" 8 | 9 | 10 | -------------------------------------------------------------------------------- /django_redis_engine/base.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ImproperlyConfigured 2 | from django.conf import settings 3 | 4 | import redis 5 | from .creation import DatabaseCreation 6 | from .client import DatabaseClient 7 | 8 | from djangotoolbox.db.base import ( 9 | NonrelDatabaseFeatures, 10 | NonrelDatabaseWrapper, 11 | NonrelDatabaseValidation, 12 | NonrelDatabaseIntrospection, 13 | NonrelDatabaseOperations 14 | ) 15 | 16 | from datetime import datetime 17 | 18 | class ImproperlyConfiguredWarning(Warning): 19 | pass 20 | 21 | class DatabaseFeatures(NonrelDatabaseFeatures): 22 | string_based_auto_field = True 23 | supports_dicts = True 24 | 25 | class DatabaseOperations(NonrelDatabaseOperations): 26 | compiler_module = __name__.rsplit('.', 1)[0] + '.compiler' 27 | 28 | def max_name_length(self): 29 | return 254 30 | 31 | def check_aggregate_support(self, aggregate): 32 | raise NotImplementedError("django-redis-engine does not support %r " 33 | "aggregates" % type(aggregate)) 34 | 35 | def sql_flush(self, style, tables, sequence_list): 36 | """ 37 | Returns a list of SQL statements that have to be executed to drop 38 | all `tables`. Not implemented yes, returns an empty list. 39 | """ 40 | 41 | return [] 42 | 43 | def value_to_db_date(self, value): 44 | if value is None: 45 | return None 46 | return datetime(value.year, value.month, value.day) 47 | 48 | def value_to_db_time(self, value): 49 | if value is None: 50 | return None 51 | return datetime(1, 1, 1, value.hour, value.minute, value.second, 52 | value.microsecond) 53 | 54 | 55 | class DatabaseValidation(NonrelDatabaseValidation): 56 | pass 57 | 58 | 59 | class DatabaseIntrospection(NonrelDatabaseIntrospection): 60 | """Database Introspection""" 61 | 62 | def table_names(self): 63 | """ Show defined models """ 64 | return []#self.connection.db_connection.collection_names() 65 | 66 | def sequence_list(self): 67 | # Only required for backends that support ManyToMany relations 68 | pass 69 | 70 | 71 | class DatabaseWrapper(NonrelDatabaseWrapper): 72 | safe_inserts = False 73 | wait_for_slaves = 0 74 | _connected = False 75 | 76 | def __init__(self, *args, **kwargs): 77 | super(DatabaseWrapper, self).__init__(*args, **kwargs) 78 | self.features = DatabaseFeatures(self) 79 | self.ops = DatabaseOperations(self) 80 | self.client = DatabaseClient(self) 81 | self.creation = DatabaseCreation(self) 82 | self.validation = DatabaseValidation(self) 83 | self.introspection = DatabaseIntrospection(self) 84 | 85 | def _cursor(self): 86 | 87 | try: 88 | return self._connection 89 | except: 90 | pass 91 | self._connect() 92 | return self._connection 93 | 94 | @property 95 | def db_connection(self): 96 | """ 97 | Returns the db_connection instance 98 | 99 | """ 100 | try: 101 | if self._connection is not None: 102 | return self._connection 103 | except: 104 | self._connect() 105 | return self._db_connection 106 | 107 | def _connect(self): 108 | import traceback 109 | import sys 110 | #print '-------------------' 111 | #traceback.print_stack() 112 | #print '-------------------' 113 | if not self._connected: 114 | host = self.settings_dict['HOST'] or None 115 | port = self.settings_dict.get('PORT', None) or None 116 | user = self.settings_dict.get('USER', None) 117 | password = self.settings_dict.get('PASSWORD') 118 | self.db_name = self.settings_dict['NAME'] 119 | try: 120 | self.exact_all = settings.REDIS_EXACT_ALL 121 | except AttributeError: 122 | self.exact_all = True 123 | 124 | self.safe_inserts = self.settings_dict.get('SAFE_INSERTS', False) 125 | 126 | self.wait_for_slaves = self.settings_dict.get('WAIT_FOR_SLAVES', 0) 127 | slave_okay = self.settings_dict.get('SLAVE_OKAY', False) 128 | 129 | try: 130 | if host is not None: 131 | assert isinstance(host, basestring), \ 132 | 'If set, HOST must be a string' 133 | 134 | if port: 135 | try: 136 | port = int(port) 137 | except ValueError: 138 | raise ImproperlyConfigured( 139 | 'If set, PORT must be an integer') 140 | 141 | assert isinstance(self.safe_inserts, bool), \ 142 | 'If set, SAFE_INSERTS must be True or False' 143 | assert isinstance(self.wait_for_slaves, int), \ 144 | 'If set, WAIT_FOR_SLAVES must be an integer' 145 | except AssertionError, e: 146 | raise ImproperlyConfigured(e) 147 | 148 | self._connection = redis.Redis(host=host, 149 | port=port, 150 | # slave_okay=slave_okay 151 | ) 152 | 153 | if user and password: 154 | auth = self._connection[self.db_name].authenticate(user, 155 | password) 156 | if not auth: 157 | raise ImproperlyConfigured("Username and/or password for " 158 | "the Redis db are not correct") 159 | 160 | self._db_connection = self._connection#[self.db_name] 161 | 162 | 163 | self._connected = True 164 | 165 | # TODO: signal! (see Alex' backend) 166 | -------------------------------------------------------------------------------- /django_redis_engine/client.py: -------------------------------------------------------------------------------- 1 | from django.db.backends import BaseDatabaseClient 2 | 3 | class DatabaseClient(BaseDatabaseClient): 4 | executable_name = 'redis' 5 | -------------------------------------------------------------------------------- /django_redis_engine/compiler.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import re 3 | import datetime 4 | 5 | from functools import wraps 6 | 7 | from django.db.utils import DatabaseError 8 | from django.db.models.fields import NOT_PROVIDED 9 | from django.db.models import F 10 | 11 | from django.db.models.sql import aggregates as sqlaggregates 12 | from django.db.models.sql.constants import MULTI, SINGLE 13 | from django.db.models.sql.where import AND, OR 14 | from django.utils.tree import Node 15 | from redis_entity import RedisEntity,split_db_type,hash_for_redis,get_hash_key,get_set_key,get_list_key,enpickle,unpickle 16 | 17 | from index_utils import get_indexes,create_indexes,delete_indexes,filter_with_index,isiterable 18 | 19 | import pickle 20 | import redis 21 | 22 | 23 | from djangotoolbox.db.basecompiler import NonrelQuery, NonrelCompiler, \ 24 | NonrelInsertCompiler, NonrelUpdateCompiler, NonrelDeleteCompiler 25 | 26 | 27 | 28 | #TODO pipeline!!!!!!!!!!!!!!!!!!!!! 29 | 30 | def safe_regex(regex, *re_args, **re_kwargs): 31 | def wrapper(value): 32 | return re.compile(regex % re.escape(value), *re_args, **re_kwargs) 33 | wrapper.__name__ = 'safe_regex (%r)' % regex 34 | return wrapper 35 | 36 | OPERATORS_MAP = { 37 | 'exact': lambda val: val, 38 | # 'iexact': safe_regex('^%s$', re.IGNORECASE), 39 | # 'startswith': safe_regex('^%s'), 40 | # 'istartswith': safe_regex('^%s', re.IGNORECASE), 41 | # 'endswith': safe_regex('%s$'), 42 | # 'iendswith': safe_regex('%s$', re.IGNORECASE), 43 | # 'contains': safe_regex('%s'), 44 | # 'icontains': safe_regex('%s', re.IGNORECASE), 45 | # 'regex': lambda val: re.compile(val), 46 | # 'iregex': lambda val: re.compile(val, re.IGNORECASE), 47 | # 'gt': lambda val: {'$gt': val}, 48 | # 'gte': lambda val: {'$gte': val}, 49 | # 'lt': lambda val: {'$lt': val}, 50 | # 'lte': lambda val: {'$lte': val}, 51 | # 'range': lambda val: {'$gte': val[0], '$lte': val[1]}, 52 | # 'year': lambda val: {'$gte': val[0], '$lt': val[1]}, 53 | # 'isnull': lambda val: None if val else {'$ne': None}, 54 | 'in': lambda val: {'$in': val}, 55 | } 56 | 57 | NEGATED_OPERATORS_MAP = { 58 | 'exact': lambda val: {'$ne': val}, 59 | 'gt': lambda val: {'$lte': val}, 60 | 'gte': lambda val: {'$lt': val}, 61 | 'lt': lambda val: {'$gte': val}, 62 | 'lte': lambda val: {'$gt': val}, 63 | 'isnull': lambda val: {'$ne': None} if val else None, 64 | 'in': lambda val: val 65 | } 66 | 67 | 68 | def first(test_func, iterable): 69 | for item in iterable: 70 | if test_func(item): 71 | return item 72 | 73 | def safe_call(func): 74 | @wraps(func) 75 | def wrapper(*args, **kwargs): 76 | try: 77 | return func(*args, **kwargs) 78 | except Exception,e: 79 | raise DatabaseError, DatabaseError(str(e)), sys.exc_info()[2] 80 | return wrapper 81 | 82 | 83 | class DBQuery(NonrelQuery): 84 | # ---------------------------------------------- 85 | # Public API 86 | # ---------------------------------------------- 87 | def __init__(self, compiler, fields): 88 | super(DBQuery, self).__init__(compiler, fields) 89 | #print fields 90 | #print dir(self.query.get_meta()) 91 | self.db_table = self.query.get_meta().db_table 92 | self.indexes = get_indexes() 93 | self.indexes_for_model = self.indexes.get(self.query.model,{}) 94 | self._collection = self.connection.db_connection 95 | self.db_name = self.connection.db_name 96 | #self.connection.exact_all 97 | self._ordering = [] 98 | self.db_query = {} 99 | 100 | # This is needed for debugging 101 | def __repr__(self): 102 | return '' % (self.db_query, self._ordering) 103 | 104 | @property 105 | def collection(self): 106 | return self._collection 107 | 108 | def fetch(self, low_mark, high_mark): 109 | 110 | results = self._get_results() 111 | #print 'here results ',results 112 | primarykey_column = self.query.get_meta().pk.column 113 | for e_id in results: 114 | yield RedisEntity(e_id,self._collection,self.db_table,primarykey_column,self.query.get_meta(),self.db_name) 115 | 116 | @safe_call 117 | def count(self, limit=None): #TODO is this right? 118 | results = self._get_results() 119 | if limit is not None: 120 | results = results[:limit] 121 | return len(results) 122 | 123 | @safe_call 124 | def delete(self): 125 | 126 | db_table = self.query.get_meta().db_table 127 | results = self._get_results() 128 | 129 | pipeline = self._collection.pipeline(transaction = False) 130 | for res in results: 131 | pipeline.hgetall(get_hash_key(self.db_name,db_table,res)) 132 | hmaps_ret = pipeline.execute() 133 | hmaps = ((results[n],hmaps_ret[n]) for n in range(len(hmaps_ret))) 134 | 135 | pipeline = self._collection.pipeline(transaction = False) 136 | for res,hmap in hmaps: 137 | pipeline.delete(get_hash_key(self.db_name,db_table,res)) 138 | for field,val in hmap.iteritems(): 139 | val = unpickle(val) 140 | if val is not None: 141 | #INDEXES 142 | if field in self.indexes_for_model or self.connection.exact_all: 143 | try: 144 | indexes_for_field = self.indexes_for_model[field] 145 | except KeyError: 146 | indexes_for_field = () 147 | if 'exact' not in indexes_for_field and self.connection.exact_all: 148 | indexes_for_field += 'exact', 149 | delete_indexes( field, 150 | val, 151 | indexes_for_field, 152 | pipeline, 153 | get_hash_key(self.db_name,db_table,res), 154 | db_table, 155 | res, 156 | self.db_name, 157 | ) 158 | pipeline.srem(self.db_name+'_'+db_table+'_ids' ,res) 159 | pipeline.execute() 160 | 161 | 162 | @safe_call 163 | def order_by(self, ordering): 164 | if len(ordering) > 1: 165 | raise DatabaseError('Only one order is allowed') 166 | for order in ordering: 167 | if order.startswith('-'): 168 | order, direction = order[1:], 'desc' 169 | else: 170 | direction = 'asc' 171 | if order == self.query.get_meta().pk.column: 172 | order = '_id' 173 | else: 174 | pass #raise DatabaseError('You can only order by PK') TODO check when order index support is active 175 | self._ordering.append((order, direction)) 176 | return self 177 | 178 | @safe_call 179 | def add_filter(self, column, lookup_type, negated, db_type, value): 180 | """add filter 181 | used by default add_filters implementation 182 | 183 | """ 184 | #print "ADD FILTER -- ",column, lookup_type, negated, db_type, value 185 | if column == self.query.get_meta().pk.column: 186 | if lookup_type in ('exact','in'): 187 | #print "cisiamo" 188 | #print "db_query?" 189 | #print self.db_query 190 | try: 191 | self.db_query[column][lookup_type] 192 | raise DatabaseError("You can't apply multiple AND filters " #Double filter on pk 193 | "on the primary key. " 194 | "Did you mean __in=[...]?") 195 | 196 | except KeyError: 197 | self.db_query.update({column:{lookup_type:value}}) 198 | 199 | else: 200 | if lookup_type in ('exact','in'): 201 | if not self.connection.exact_all and 'exact' not in self.indexes_for_model.get(column,()): 202 | raise DatabaseError('Lookup %s on column %s is not allowed (have you tried redis_indexes? )' % (lookup_type,column)) 203 | else:self.db_query.update({column:{lookup_type:value}}) 204 | else: 205 | if lookup_type in self.indexes_for_model.get(column,()): 206 | self.db_query.update({column:{lookup_type:value}}) 207 | 208 | else: 209 | raise DatabaseError('Lookup %s on column %s is not allowed (have you tried redis_indexes? )' % (lookup_type,column)) 210 | 211 | 212 | def _get_results(self): 213 | """ 214 | see self.db_query, lookup parameters format: {'column': {lookup:value}} 215 | 216 | """ 217 | pk_column = self.query.get_meta().pk.column 218 | db_table = self.query.get_meta().db_table 219 | 220 | results = self._collection.smembers(self.db_name+'_'+db_table+'_ids') 221 | 222 | 223 | for column,filteradd in self.db_query.iteritems(): 224 | lookup,value = filteradd.popitem()#TODO tuple better? 225 | 226 | if pk_column == column: 227 | if lookup == 'in': #TODO meglio?? 228 | results = results & set(value) #IN filter 229 | elif lookup == 'exact': 230 | results = results & set([value,]) 231 | 232 | else: 233 | if lookup == 'exact': 234 | results = results & self._collection.smembers(get_set_key(self.db_name,db_table,column,value)) 235 | elif lookup == 'in': #ListField or empty 236 | tempset = set() 237 | for v in value: 238 | tempset = tempset.union(self._collection.smembers(get_set_key(self.db_name,db_table,column,v) ) ) 239 | results = results & tempset 240 | else: 241 | tempset = filter_with_index(lookup,value,self._collection,db_table,column,self.db_name) 242 | if tempset is not None: 243 | results = results & tempset 244 | else: 245 | results = set() 246 | 247 | 248 | if self._ordering: 249 | if self._ordering[0][1] == 'desc': 250 | results.reverse() 251 | 252 | if self.query.low_mark > 0 and self.query.high_mark is not None: 253 | results = list(results)[self.query.low_mark:self.query.high_mark] 254 | elif self.query.low_mark > 0: 255 | 256 | results = list(results)[self.query.low_mark:] 257 | 258 | elif self.query.high_mark is not None: 259 | results = list(results)[:self.query.high_mark] 260 | 261 | return list(results) 262 | 263 | class SQLCompiler(NonrelCompiler): 264 | """ 265 | A simple query: no joins, no distinct, etc. 266 | """ 267 | query_class = DBQuery 268 | 269 | def _split_db_type(self, db_type): 270 | try: 271 | db_type, db_subtype = db_type.split(':', 1) 272 | except ValueError: 273 | db_subtype = None 274 | return db_type, db_subtype 275 | 276 | @safe_call # see #7 277 | def convert_value_for_db(self, db_type, value): 278 | #print db_type,' ',value 279 | if db_type is None or value is None: 280 | return value 281 | 282 | db_type, db_subtype = self._split_db_type(db_type) 283 | if db_subtype is not None: 284 | if isinstance(value, (set, list, tuple)): 285 | 286 | return [self.convert_value_for_db(db_subtype, subvalue) 287 | for subvalue in value] 288 | elif isinstance(value, dict): 289 | return dict((key, self.convert_value_for_db(db_subtype, subvalue)) 290 | for key, subvalue in value.iteritems()) 291 | 292 | if isinstance(value, (set, list, tuple)): 293 | # most likely a list of ObjectIds when doing a .delete() query 294 | return [self.convert_value_for_db(db_type, val) for val in value] 295 | 296 | if db_type == 'objectid': 297 | return value 298 | return value 299 | 300 | @safe_call # see #7 301 | def convert_value_from_db(self, db_type, value): 302 | if db_type is None: 303 | return value 304 | 305 | if value is None or value is NOT_PROVIDED: 306 | # ^^^ it is *crucial* that this is not written as 'in (None, NOT_PROVIDED)' 307 | # because that would call value's __eq__ method, which in case value 308 | # is an instance of serializer.LazyModelInstance does a database query. 309 | return None 310 | 311 | db_type, db_subtype = self._split_db_type(db_type) 312 | if db_subtype is not None: 313 | for field, type_ in [('SetField', set), ('ListField', list)]: 314 | if db_type == field: 315 | return type_(self.convert_value_from_db(db_subtype, subvalue) 316 | for subvalue in value) 317 | if db_type == 'DictField': 318 | return dict((key, self.convert_value_from_db(db_subtype, subvalue)) 319 | for key, subvalue in value.iteritems()) 320 | 321 | if db_type == 'objectid': 322 | return unicode(value) 323 | 324 | if db_type == 'date': 325 | return datetime.date(value.year, value.month, value.day) 326 | 327 | if db_type == 'time': 328 | return datetime.time(value.hour, value.minute, value.second, 329 | value.microsecond) 330 | return value 331 | 332 | def insert_params(self): 333 | conn = self.connection 334 | params = {'safe': conn.safe_inserts} 335 | if conn.wait_for_slaves: 336 | params['w'] = conn.wait_for_slaves 337 | return params 338 | 339 | @property 340 | def _collection(self): 341 | #TODO multi db 342 | return self.connection.db_connection 343 | @property 344 | def db_name(self): 345 | return self.connection.db_name 346 | 347 | def _save(self, data, return_id=False): 348 | 349 | db_table = self.query.get_meta().db_table 350 | indexes = get_indexes() 351 | indexes_for_model = indexes.get(self.query.model,{}) 352 | 353 | pipeline = self._collection.pipeline(transaction = False) 354 | 355 | h_map = {} 356 | h_map_old = {} 357 | 358 | if '_id' in data: 359 | pk = data['_id'] 360 | new = False 361 | h_map_old = self._collection.hgetall(get_hash_key(self.db_name,db_table,pk)) 362 | else: 363 | pk = self._collection.incr(self.db_name+'_'+db_table+"_id") 364 | new = True 365 | 366 | for key,value in data.iteritems(): 367 | 368 | if new: 369 | old = None 370 | h_map[key] = pickle.dumps(value) 371 | else: 372 | if key == "_id": continue 373 | old = pickle.loads(h_map_old[key]) 374 | 375 | if old != value: 376 | h_map[key] = pickle.dumps(value) 377 | 378 | if key in indexes_for_model or self.connection.exact_all: 379 | try: 380 | indexes_for_field = indexes_for_model[key] 381 | except KeyError: 382 | indexes_for_field = () 383 | if 'exact' not in indexes_for_field and self.connection.exact_all: 384 | indexes_for_field += 'exact', 385 | create_indexes( key, 386 | value, 387 | old, 388 | indexes_for_field, 389 | pipeline, 390 | db_table+'_'+str(pk), 391 | db_table, 392 | pk, 393 | self.db_name, 394 | ) 395 | 396 | if '_id' not in data: pipeline.sadd(self.db_name+'_'+db_table+"_ids" ,pk) 397 | 398 | pipeline.hmset(get_hash_key(self.db_name,db_table,pk),h_map) 399 | pipeline.execute() 400 | if return_id: 401 | return unicode(pk) 402 | 403 | def execute_sql(self, result_type=MULTI): 404 | """ 405 | Handles aggregate/count queries 406 | """ 407 | 408 | 409 | raise NotImplementedError('Not implemented') 410 | 411 | 412 | class SQLInsertCompiler(NonrelInsertCompiler, SQLCompiler): 413 | @safe_call 414 | def insert(self, data, return_id=False): 415 | pk_column = self.query.get_meta().pk.column 416 | try: 417 | data['_id'] = data.pop(pk_column) 418 | except KeyError: 419 | pass 420 | return self._save(data, return_id) 421 | 422 | class SQLUpdateCompiler(NonrelUpdateCompiler, SQLCompiler): 423 | pass 424 | class SQLDeleteCompiler(NonrelDeleteCompiler, SQLCompiler): 425 | pass 426 | -------------------------------------------------------------------------------- /django_redis_engine/creation.py: -------------------------------------------------------------------------------- 1 | from djangotoolbox.db.base import NonrelDatabaseCreation 2 | 3 | TEST_DATABASE_PREFIX = 'test_' 4 | 5 | class DatabaseCreation(NonrelDatabaseCreation): 6 | """Database Creation class. 7 | """ 8 | data_types = { 9 | 'DateTimeField': 'datetime', 10 | 'DateField': 'date', 11 | 'TimeField': 'time', 12 | 'FloatField': 'float', 13 | 'EmailField': 'unicode', 14 | 'URLField': 'unicode', 15 | 'BooleanField': 'bool', 16 | 'NullBooleanField': 'bool', 17 | 'CharField': 'unicode', 18 | 'CommaSeparatedIntegerField': 'unicode', 19 | 'IPAddressField': 'unicode', 20 | 'SlugField': 'unicode', 21 | 'FileField': 'unicode', 22 | 'FilePathField': 'unicode', 23 | 'TextField': 'unicode', 24 | 'XMLField': 'unicode', 25 | 'IntegerField': 'int', 26 | 'SmallIntegerField': 'int', 27 | 'PositiveIntegerField': 'int', 28 | 'PositiveSmallIntegerField': 'int', 29 | 'BigIntegerField': 'int', 30 | 'GenericAutoField': 'objectid', 31 | 'StringForeignKey': 'objectid', 32 | 'AutoField': 'objectid', 33 | 'RelatedAutoField': 'objectid', 34 | 'OneToOneField': 'int', 35 | 'DecimalField': 'float', 36 | } 37 | 38 | def sql_indexes_for_field(self, model, field, **kwargs): 39 | """Create Indexes for field in model. 40 | 41 | Indexes are created rundime, no need for this. 42 | Returns an empty List. (Django Compatibility) 43 | """ 44 | 45 | """if field.db_index: 46 | kwargs = {} 47 | opts = model._meta 48 | col = getattr(self.connection.db_connection, opts.db_table) 49 | descending = getattr(opts, "descending_indexes", []) 50 | direction = (field.attname in descending and -1) or 1 51 | kwargs["unique"] = field.unique 52 | col.ensure_index([(field.name, direction)], **kwargs)""" 53 | return [] 54 | 55 | def index_fields_group(self, model, group, **kwargs): 56 | """Create indexes for fields in group that belong to model. 57 | TODO: Necessary? 58 | """ 59 | """if not isinstance(group, dict): 60 | raise TypeError("Indexes group has to be instance of dict") 61 | 62 | fields = group.pop("fields") 63 | 64 | if not isinstance(fields, (list, tuple)): 65 | raise TypeError("index_together fields has to be instance of list") 66 | 67 | opts = model._meta 68 | col = getattr(self.connection.db_connection, opts.db_table) 69 | checked_fields = [] 70 | model_fields = [ f.name for f in opts.local_fields] 71 | 72 | for field in fields: 73 | field_name = field 74 | direction = 1 75 | if isinstance(field, (tuple, list)): 76 | field_name = field[0] 77 | direction = (field[1] and 1) or -1 78 | if not field_name in model_fields: 79 | from django.db.models.fields import FieldDoesNotExist 80 | raise FieldDoesNotExist('%s has no field named %r' % (opts.object_name, field_name)) 81 | checked_fields.append((field_name, direction)) 82 | col.ensure_index(checked_fields, **group)""" 83 | 84 | def sql_indexes_for_model(self, model, *args, **kwargs): 85 | """Creates ``model`` indexes. 86 | 87 | Probably not necessary 88 | TODO Erase? 89 | """ 90 | """if not model._meta.managed or model._meta.proxy: 91 | return [] 92 | fields = [f for f in model._meta.local_fields if f.db_index] 93 | if not fields and not hasattr(model._meta, "index_together") and not hasattr(model._meta, "unique_together"): 94 | return [] 95 | print "Installing index for %s.%s model" % (model._meta.app_label, model._meta.object_name) 96 | for field in fields: 97 | self.sql_indexes_for_field(model, field) 98 | for group in getattr(model._meta, "index_together", []): 99 | self.index_fields_group(model, group) 100 | 101 | #unique_together support 102 | unique_together = getattr(model._meta, "unique_together", []) 103 | # Django should do this, I just wanted to be REALLY sure. 104 | if len(unique_together) > 0 and isinstance(unique_together[0], basestring): 105 | unique_together = (unique_together,) 106 | for fields in unique_together: 107 | group = { "fields" : fields, "unique" : True} 108 | self.index_fields_group(model, group)""" 109 | return [] 110 | 111 | def sql_create_model(self, model, *args, **kwargs): 112 | """TODO delete... 113 | """ 114 | return [], {} 115 | 116 | 117 | def set_autocommit(self): 118 | "Make sure a connection is in autocommit mode." 119 | pass 120 | 121 | def create_test_db(self, verbosity=1, autoclobber=False): 122 | if self.connection.settings_dict.get('TEST_NAME'): 123 | test_database_name = self.connection.settings_dict['TEST_NAME'] 124 | elif 'NAME' in self.connection.settings_dict: 125 | test_database_name = TEST_DATABASE_PREFIX + self.connection.settings_dict['NAME'] 126 | elif 'DATABASE_NAME' in self.connection.settings_dict: 127 | if self.connection.settings_dict['DATABASE_NAME'].startswith(TEST_DATABASE_PREFIX): 128 | test_database_name = self.connection.settings_dict['DATABASE_NAME'] 129 | else: 130 | test_database_name = TEST_DATABASE_PREFIX + \ 131 | self.connection.settings_dict['DATABASE_NAME'] 132 | else: 133 | raise ValueError("Name for test database not defined") 134 | 135 | self.connection.settings_dict['NAME'] = test_database_name 136 | # This is important. Here we change the settings so that all other code 137 | # thinks that the chosen database is now the test database. This means 138 | # that nothing needs to change in the test code for working with 139 | # connections, databases and collections. It will appear the same as 140 | # when working with non-test code. 141 | 142 | # In this phase it will only drop the database if it already existed 143 | # which could potentially happen if the test database was created but 144 | # was never dropped at the end of the tests 145 | self._drop_database(test_database_name) 146 | 147 | return test_database_name 148 | 149 | def destroy_test_db(self, old_database_name, verbosity=1): 150 | """ 151 | Destroy a test database, prompting the user for confirmation if the 152 | database already exists. Returns the name of the test database created. 153 | """ 154 | if verbosity >= 1: 155 | print "Destroying test database '%s'..." % self.connection.alias 156 | test_database_name = self.connection.settings_dict['NAME'] 157 | self._drop_database(test_database_name) 158 | self.connection.settings_dict['NAME'] = old_database_name 159 | 160 | def _drop_database(self, database_name): 161 | """Drops the database with name database_name 162 | 163 | """ 164 | for k in self.connection._cursor().keys(database_name+'*'): 165 | del self.connection._cursor()[k] 166 | -------------------------------------------------------------------------------- /django_redis_engine/index_utils.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.utils.importlib import import_module 3 | from md5 import md5 4 | from redis_entity import * 5 | _MODULE_NAMES = getattr(settings, 'REDIS_SETTINGS_MODULES', ()) 6 | from redis.exceptions import WatchError 7 | 8 | #TODO update might overwrite field indexes 9 | 10 | INDEX_KEY_INFIX = "redis_index" 11 | 12 | SWITH_INDEX_SEPARATOR = '\x00' 13 | 14 | isiterable = lambda obj: getattr(obj, '__iter__', False) 15 | 16 | import datetime 17 | 18 | def val_for_insert(d): 19 | if isinstance(d,unicode): 20 | d = d 21 | elif isinstance(d,basestring) : d = unicode(d.decode('utf-8')) 22 | else: d = unicode(d) 23 | return d 24 | 25 | 26 | def get_indexes(): 27 | indexes = {} 28 | for name in _MODULE_NAMES: 29 | try: 30 | indexes.update(import_module(name).INDEXES) 31 | except (ImportError, AttributeError): 32 | pass 33 | 34 | return indexes 35 | 36 | def prepare_value_for_index(index,val): 37 | if index == 'startswith': return val 38 | if index == 'istartswith': return val.lower() 39 | if index == 'endswith': return val[::-1] 40 | if index == 'iendswith': return val.lower()[::-1] 41 | if index in ('gt','gte','lt','lte'): 42 | if isinstance(val,datetime.datetime): 43 | return "%04d%02d%02d%02d%02d" % (val.year,val.month,val.day,val.hour,val.minute) 44 | if isinstance(val,datetime.time): 45 | return "%02d%02d" % (val.hour,val.minute) 46 | if isinstance(val,datetime.date): 47 | return "%04d%02d%02d" % (val.year,val.month,val.day) 48 | if isinstance(val,int): 49 | return "%20d" % val 50 | return val 51 | 52 | 53 | 54 | def create_indexes(column,data,old,indexes,conn,hash_record,table,pk,db_name): 55 | for index in indexes: 56 | if index in ('startswith','istartswith','endswith','iendswith','gt','gte','lt','lte'): 57 | if old is not None: 58 | if not isiterable(old): 59 | old = (old,) 60 | for d in old: 61 | d = prepare_value_for_index(index,d) 62 | conn.zrem(get_zset_index_key(db_name,table,INDEX_KEY_INFIX,column,index), 63 | d+SWITH_INDEX_SEPARATOR+str(pk)) 64 | if not isiterable(data): 65 | data = (data,) 66 | for d in data: 67 | d = val_for_insert(d) 68 | d = prepare_value_for_index(index,d) 69 | conn.zadd(get_zset_index_key(db_name,table,INDEX_KEY_INFIX,column,index), 70 | d+SWITH_INDEX_SEPARATOR+str(pk),0) 71 | if index == 'exact': 72 | if old is not None: 73 | if not isiterable(old): 74 | old = (old,) 75 | for d in old: 76 | conn.srem(get_set_key(db_name,table,column,d),str(pk)) 77 | if not isiterable(data): 78 | data = (data,) 79 | for d in data: 80 | d = val_for_insert(d) 81 | conn.sadd(get_set_key(db_name,table,column,d),pk) 82 | 83 | 84 | def delete_indexes(column,data,indexes,conn,hash_record,table,pk,db_name): 85 | for index in indexes: 86 | if index in ('startswith','istartswith','endswith','iendswith','gt','gte','lt','lte'): 87 | if not isiterable(data): 88 | data = (data,) 89 | for d in data: 90 | d = val_for_insert(d) 91 | d = prepare_value_for_index(index,d) 92 | conn.zrem(get_zset_index_key(db_name,table,INDEX_KEY_INFIX,column,index), 93 | d+SWITH_INDEX_SEPARATOR+str(pk)) 94 | if index == 'exact': 95 | if not isiterable(data): 96 | data = (data,) 97 | for d in data: 98 | d = val_for_insert(d) 99 | conn.srem(get_set_key(db_name,table,column,d),str(pk)) 100 | 101 | def filter_with_index(lookup,value,conn,table,column,db_name): 102 | if lookup in ('startswith','istartswith','endswith','iendswith',): 103 | value = val_for_insert(value) 104 | v = prepare_value_for_index(lookup,value) 105 | 106 | #v2 = v[:-1]+chr(ord(v[-1])+1) #last letter=next(last letter) 107 | key = get_zset_index_key(db_name,table,INDEX_KEY_INFIX,column,lookup) 108 | 109 | pipeline = conn.pipeline() 110 | conn.zadd(key,v,0) 111 | #pipeline.zadd(key,v2,0) 112 | #pipeline.execute() 113 | while True: 114 | try: 115 | conn.watch(key) 116 | up = conn.zrank(key,v) 117 | #down = conn.zrank(key,v2) 118 | 119 | pipeline.zrange(key,up+1,-1)#down-1) 120 | pipeline.zrem(key,v) 121 | #pipeline.zrem(key,v2) 122 | 123 | l = pipeline.execute() 124 | #print l 125 | r = l[0] 126 | #print l 127 | #print 'erre: ',r 128 | #print 'second pipeline',pipeline.execute() 129 | ret = set() 130 | for i in r: 131 | i = unicode(i.decode('utf8')) 132 | if i.startswith(v): 133 | splitted_string = i.split(SWITH_INDEX_SEPARATOR) 134 | if len(splitted_string) > 1: 135 | ret.add(splitted_string[-1]) 136 | else: 137 | break 138 | return ret 139 | except WatchError: 140 | pass 141 | # print pipeline.execute() 142 | 143 | elif lookup in ('gt','gte','lt','lte'): 144 | value = val_for_insert(value) 145 | v = prepare_value_for_index(lookup,value) 146 | key = get_zset_index_key(db_name,table,INDEX_KEY_INFIX,column,lookup) 147 | pipeline = conn.pipeline() 148 | conn.zadd(key,v,0) 149 | while True: 150 | try: 151 | conn.watch(key) 152 | up = conn.zrank(key,v) 153 | if lookup in ('lt','lte'): 154 | pipeline.zrange(key,0,up+1) 155 | else: 156 | pipeline.zrange(key,up+1,-1) 157 | pipeline.zrem(key,v) 158 | l = pipeline.execute() 159 | r = l[0] 160 | ret = set() 161 | for i in r: 162 | i = unicode(i.decode('utf8')) 163 | splitted_string = i.split(SWITH_INDEX_SEPARATOR) 164 | if len(splitted_string) > 0 and\ 165 | (lookup in ('gte','lte') or\ 166 | "".join(splitted_string[:-1]) != value): 167 | ret.add(splitted_string[-1]) 168 | return ret 169 | except WatchError: 170 | pass 171 | else: 172 | raise Exception('Lookup type not supported') #TODO check at index creation? 173 | 174 | 175 | 176 | # 177 | -------------------------------------------------------------------------------- /django_redis_engine/redis_entity.py: -------------------------------------------------------------------------------- 1 | from django.db.models.fields import FieldDoesNotExist 2 | from md5 import md5 3 | import pickle 4 | 5 | class RedisEntity(object): 6 | def __init__(self,e_id,connection,db_table, pkcolumn, querymeta, db_name,empty): 7 | self.id = e_id 8 | self.connection = connection 9 | self.db_table = db_table 10 | self.pkcolumn = pkcolumn 11 | self.querymeta = querymeta 12 | self.db_name = db_name 13 | self.empty = empty 14 | if not empty: 15 | self.data = self.connection.hgetall(get_hash_key(self.db_name,self.db_table,self.id)) 16 | 17 | 18 | def get(self,what,value): 19 | 20 | if self.empty: return '' 21 | if what in self.data: 22 | #print self.data,self.data[what] 23 | return unpickle(self.data[what]) 24 | if what == self.pkcolumn: 25 | return self.id 26 | else: 27 | return unpickle(self.connection.hget(get_hash_key(self.db_name,self.db_table,self.id), what)) 28 | 29 | 30 | def split_db_type(db_type): 31 | #TODO move somewhere else 32 | try: 33 | db_type, db_subtype = db_type.split(':', 1) 34 | except ValueError: 35 | db_subtype = None 36 | return db_type, db_subtype 37 | 38 | def get_hash_key(db_name,db_table,pk): 39 | return db_name+'_'+db_table+'_'+str(pk) 40 | 41 | def get_zset_index_key(db_name,db_table,infix,column,index): 42 | return db_name+'_'+db_table +'_' + infix + '_' + column + '_'+index 43 | 44 | def get_list_key(db_name,db_table,key,pk): 45 | return db_name+'_'+db_table+'_'+key+'_'+str(pk) 46 | 47 | 48 | def get_set_key(db_name,db_table,key,value): 49 | return db_name+'_'+db_table+'_'+key+'_'+hash_for_redis(value) 50 | 51 | def unpickle(val): 52 | if val is None: 53 | return None 54 | else: 55 | return pickle.loads(val) 56 | 57 | def enpickle(val): 58 | if val is None: 59 | return None 60 | else: 61 | return pickle.dumps(val) 62 | 63 | 64 | def hash_for_redis(val): 65 | if isinstance(val,unicode): 66 | return md5(val.encode('utf-8')).hexdigest() 67 | return md5(str(val)).hexdigest() 68 | -------------------------------------------------------------------------------- /django_redis_engine/redis_transaction.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module implements a transaction manager that can be used to define 3 | transaction handling in a request or view function. It is used by transaction 4 | control middleware and decorators. 5 | 6 | The transaction manager can be in managed or in auto state. Auto state means the 7 | system is using a commit-on-save strategy (actually it's more like 8 | commit-on-change). As soon as the .save() or .delete() (or related) methods are 9 | called, a commit is made. 10 | 11 | Managed transactions don't do those commits, but will need some kind of manual 12 | or implicit commits or rollbacks. 13 | """ 14 | import sys 15 | 16 | try: 17 | from functools import wraps 18 | except ImportError: 19 | from django.utils.functional import wraps # Python 2.4 fallback. 20 | 21 | from django.conf import settings 22 | from django.db import connections, DEFAULT_DB_ALIAS 23 | 24 | 25 | class RedisTransactionManagementError(Exception): 26 | """ 27 | This exception is thrown when something bad happens with transaction 28 | management. 29 | """ 30 | pass 31 | 32 | def redis_enter_transaction_management(managed=True, using=None): 33 | """ 34 | Enters transaction management for a running thread. It must be balanced with 35 | the appropriate redis_leave_transaction_management call, since the actual state is 36 | managed as a stack. 37 | 38 | The state and dirty flag are carried over from the surrounding block or 39 | from the settings, if there is no surrounding block (dirty is always false 40 | when no current block is running). 41 | """ 42 | if using is None: 43 | using = DEFAULT_DB_ALIAS 44 | connection = connections[using] 45 | connection.redis_enter_transaction_management(managed) 46 | 47 | def redis_leave_transaction_management(using=None): 48 | """ 49 | Leaves transaction management for a running thread. A dirty flag is carried 50 | over to the surrounding block, as a commit will commit all changes, even 51 | those from outside. (Commits are on connection level.) 52 | """ 53 | if using is None: 54 | using = DEFAULT_DB_ALIAS 55 | connection = connections[using] 56 | connection.redis_leave_transaction_management(using) 57 | 58 | def is_dirty(using=None): 59 | """ 60 | Returns True if the current transaction requires a commit for changes to 61 | happen. 62 | """ 63 | if using is None: 64 | using = DEFAULT_DB_ALIAS 65 | connection = connections[using] 66 | return connection.is_dirty() 67 | 68 | def set_dirty(using=None): 69 | """ 70 | Sets a dirty flag for the current thread and code streak. This can be used 71 | to decide in a managed block of code to decide whether there are open 72 | changes waiting for commit. 73 | """ 74 | if using is None: 75 | using = DEFAULT_DB_ALIAS 76 | connection = connections[using] 77 | connection.set_dirty() 78 | 79 | def set_clean(using=None): 80 | """ 81 | Resets a dirty flag for the current thread and code streak. This can be used 82 | to decide in a managed block of code to decide whether a commit or rollback 83 | should happen. 84 | """ 85 | if using is None: 86 | using = DEFAULT_DB_ALIAS 87 | connection = connections[using] 88 | connection.set_clean() 89 | 90 | def clean_savepoints(using=None): 91 | if using is None: 92 | using = DEFAULT_DB_ALIAS 93 | connection = connections[using] 94 | connection.clean_savepoints() 95 | 96 | def is_managed(using=None): 97 | """ 98 | Checks whether the transaction manager is in manual or in auto state. 99 | """ 100 | if using is None: 101 | using = DEFAULT_DB_ALIAS 102 | connection = connections[using] 103 | return connection.is_managed() 104 | 105 | def managed(flag=True, using=None): 106 | """ 107 | Puts the transaction manager into a manual state: managed transactions have 108 | to be committed explicitly by the user. If you switch off transaction 109 | management and there is a pending commit/rollback, the data will be 110 | commited. 111 | """ 112 | if using is None: 113 | using = DEFAULT_DB_ALIAS 114 | connection = connections[using] 115 | connection.redis_managed(flag) 116 | 117 | def commit_unless_managed(using=None): 118 | """ 119 | Commits changes if the system is not in managed transaction mode. 120 | """ 121 | if using is None: 122 | using = DEFAULT_DB_ALIAS 123 | connection = connections[using] 124 | connection.commit_unless_managed() 125 | 126 | def rollback_unless_managed(using=None): 127 | """ 128 | Rolls back changes if the system is not in managed transaction mode. 129 | """ 130 | if using is None: 131 | using = DEFAULT_DB_ALIAS 132 | connection = connections[using] 133 | connection.rollback_unless_managed() 134 | 135 | def commit(using=None): 136 | """ 137 | Does the commit itself and resets the dirty flag. 138 | """ 139 | if using is None: 140 | using = DEFAULT_DB_ALIAS 141 | connection = connections[using] 142 | connection.redis_commit() 143 | 144 | def rollback(using=None): 145 | """ 146 | This function does the rollback itself and resets the dirty flag. 147 | """ 148 | if using is None: 149 | using = DEFAULT_DB_ALIAS 150 | connection = connections[using] 151 | connection.rollback() 152 | 153 | def savepoint(using=None): 154 | """ 155 | Creates a savepoint (if supported and required by the backend) inside the 156 | current transaction. Returns an identifier for the savepoint that will be 157 | used for the subsequent rollback or commit. 158 | """ 159 | if using is None: 160 | using = DEFAULT_DB_ALIAS 161 | connection = connections[using] 162 | return connection.savepoint() 163 | 164 | def savepoint_rollback(sid, using=None): 165 | """ 166 | Rolls back the most recent savepoint (if one exists). Does nothing if 167 | savepoints are not supported. 168 | """ 169 | if using is None: 170 | using = DEFAULT_DB_ALIAS 171 | connection = connections[using] 172 | connection.savepoint_rollback(sid) 173 | 174 | def savepoint_redis_commit(sid, using=None): 175 | """ 176 | Commits the most recent savepoint (if one exists). Does nothing if 177 | savepoints are not supported. 178 | """ 179 | if using is None: 180 | using = DEFAULT_DB_ALIAS 181 | connection = connections[using] 182 | connection.savepoint_redis_commit(sid) 183 | 184 | ############## 185 | # DECORATORS # 186 | ############## 187 | 188 | class RedisTransaction(object): 189 | """ 190 | Acts as either a decorator, or a context manager. If it's a decorator it 191 | takes a function and returns a wrapped function. If it's a contextmanager 192 | it's used with the ``with`` statement. In either event entering/exiting 193 | are called before and after, respectively, the function/block is executed. 194 | 195 | autocommit, commit_on_success, and commit_manually contain the 196 | implementations of entering and exiting. 197 | """ 198 | def __init__(self, entering, exiting, using): 199 | self.entering = entering 200 | self.exiting = exiting 201 | self.using = using 202 | 203 | def __enter__(self): 204 | self.entering(self.using) 205 | 206 | def __exit__(self, exc_type, exc_value, traceback): 207 | self.exiting(exc_value, self.using) 208 | 209 | def __call__(self, func): 210 | @wraps(func) 211 | def inner(*args, **kwargs): 212 | # Once we drop support for Python 2.4 this block should become: 213 | # with self: 214 | # func(*args, **kwargs) 215 | self.__enter__() 216 | try: 217 | res = func(*args, **kwargs) 218 | except: 219 | self.__exit__(*sys.exc_info()) 220 | raise 221 | else: 222 | self.__exit__(None, None, None) 223 | return res 224 | return inner 225 | 226 | def _transaction_func(entering, exiting, using): 227 | """ 228 | Takes 3 things, an entering function (what to do to start this block of 229 | transaction management), an exiting function (what to do to end it, on both 230 | success and failure, and using which can be: None, indiciating using is 231 | DEFAULT_DB_ALIAS, a callable, indicating that using is DEFAULT_DB_ALIAS and 232 | to return the function already wrapped. 233 | 234 | Returns either a RedisTransaction objects, which is both a decorator and a 235 | context manager, or a wrapped function, if using is a callable. 236 | """ 237 | # Note that although the first argument is *called* `using`, it 238 | # may actually be a function; @autocommit and @autoredis_commit('foo') 239 | # are both allowed forms. 240 | if using is None: 241 | using = DEFAULT_DB_ALIAS 242 | if callable(using): 243 | return RedisTransaction(entering, exiting, DEFAULT_DB_ALIAS)(using) 244 | return RedisTransaction(entering, exiting, using) 245 | 246 | 247 | def autoredis_commit(using=None): 248 | """ 249 | Decorator that activates commit on save. This is Django's default behavior; 250 | this decorator is useful if you globally activated transaction management in 251 | your settings file and want the default behavior in some view functions. 252 | """ 253 | def entering(using): 254 | redis_enter_transaction_management(managed=False, using=using) 255 | managed(False, using=using) 256 | 257 | def exiting(exc_value, using): 258 | redis_leave_transaction_management(using=using) 259 | 260 | return _transaction_func(entering, exiting, using) 261 | 262 | def commit_on_success(using=None): 263 | """ 264 | This decorator activates commit on response. This way, if the view function 265 | runs successfully, a commit is made; if the viewfunc produces an exception, 266 | a rollback is made. This is one of the most common ways to do transaction 267 | control in Web apps. 268 | """ 269 | def entering(using): 270 | redis_enter_transaction_management(using=using) 271 | managed(True, using=using) 272 | 273 | def exiting(exc_value, using): 274 | try: 275 | if exc_value is not None: 276 | if is_dirty(using=using): 277 | rollback(using=using) 278 | else: 279 | if is_dirty(using=using): 280 | try: 281 | redis_commit(using=using) 282 | except: 283 | rollback(using=using) 284 | raise 285 | finally: 286 | redis_leave_transaction_management(using=using) 287 | 288 | return _transaction_func(entering, exiting, using) 289 | 290 | def commit_manually(using=None): 291 | """ 292 | Decorator that activates manual transaction control. It just disables 293 | automatic transaction control and doesn't do any commit/rollback of its 294 | own -- it's up to the user to call the commit and rollback functions 295 | themselves. 296 | """ 297 | def entering(using): 298 | redis_enter_transaction_management(using=using) 299 | #managed(True, using=using) 300 | 301 | def exiting(exc_value, using): 302 | redis_leave_transaction_management(using=using) 303 | 304 | return _transaction_func(entering, exiting, using) 305 | -------------------------------------------------------------------------------- /django_redis_engine/serializer.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models.query import QuerySet 3 | from django.utils.functional import SimpleLazyObject 4 | from django.utils.importlib import import_module 5 | 6 | def get_model_by_meta(model_meta): 7 | app, model = model_meta['_app'], model_meta['_model'] 8 | try: 9 | module = import_module(app + '.models') 10 | except ImportError: 11 | return models.get_model(app, model) 12 | else: 13 | try: 14 | return getattr(module, model) 15 | except AttributeError: 16 | raise AttributeError("Could not find model %r in module %r" % (model, module)) 17 | 18 | class LazyModelInstance(SimpleLazyObject): 19 | """ 20 | Lazy model instance. 21 | """ 22 | def __init__(self, model, pk): 23 | self.__dict__['_pk'] = pk 24 | self.__dict__['_model'] = model 25 | super(LazyModelInstance, self).__init__(self._load_data) 26 | 27 | def _load_data(self): 28 | return self._model.objects.get(pk=self._pk) 29 | 30 | def __eq__(self, other): 31 | if isinstance(other, LazyModelInstance): 32 | return self.__dict__['_pk'] == other.__dict__['_pk'] and \ 33 | self.__dict__['_model'] == other.__dict__['_model'] 34 | return super(LazyModelInstance, self).__eq__(other) 35 | 36 | 37 | class TransformDjango(SONManipulator): 38 | def transform_incoming(self, value, collection): 39 | if isinstance(value, (list, tuple, set, QuerySet)): 40 | return [self.transform_incoming(item, collection) for item in value] 41 | 42 | if isinstance(value, dict): 43 | return dict((key, self.transform_incoming(subvalue, collection)) 44 | for key, subvalue in value.iteritems()) 45 | 46 | if isinstance(value, models.Model): 47 | value.save() 48 | return { 49 | '_app' : value._meta.app_label, 50 | '_model' : value._meta.object_name, 51 | 'pk' : value.pk, 52 | '_type' : 'django' 53 | } 54 | 55 | return value 56 | 57 | def transform_outgoing(self, son, collection): 58 | if isinstance(son, (list, tuple, set)): 59 | return [self.transform_outgoing(value, collection) for value in son] 60 | 61 | if isinstance(son, dict): 62 | if son.get('_type') == 'django': 63 | return LazyModelInstance(get_model_by_meta(son), son['pk']) 64 | else: 65 | return dict((key, self.transform_outgoing(value, collection)) 66 | for key, value in son.iteritems()) 67 | return son 68 | -------------------------------------------------------------------------------- /normalize_whitespace: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Removes trailing whitespace from the file in sys.argv[1] 4 | Example: 5 | ./normalize_whitespace foo.py 6 | 7 | or: 8 | find -type f | xargs -n1 ./normalize_whitespace 9 | """ 10 | import re 11 | import sys 12 | with open(sys.argv[1]) as f: c = f.read().split('\n') 13 | with open(sys.argv[1], 'w') as f: 14 | for line in c: 15 | if not line.isspace(): 16 | f.write(line.rstrip()) 17 | f.write('\n') 18 | f.truncate(f.tell()-1) 19 | 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import codecs 3 | import django_redis_engine as distmeta 4 | 5 | 6 | CLASSIFIERS = [ 7 | 'Intended Audience :: Developers', 8 | 'License :: OSI Approved :: BSD License', 9 | 'Programming Language :: Python', 10 | 'Topic :: Internet', 11 | 'Topic :: Database', 12 | 'Topic :: Software Development :: Libraries :: Python Modules', 13 | 'Operating System :: OS Independent', 14 | ] 15 | 16 | for ver in ['2', '2.4', '2.5', '2.6', '2.7']: 17 | CLASSIFIERS.append('Programming Language :: Python :: %s' % ver) 18 | 19 | 20 | setup( 21 | name='django-redis-engine', 22 | version='.'.join(map(str, distmeta.__version__)), 23 | author=distmeta.__author__, 24 | author_email=distmeta.__contact__, 25 | license='2-clause BSD', 26 | description= "A Redis backend.", 27 | long_description=codecs.open('README.rst', 'r', 'utf-8').read(), 28 | 29 | platforms=['any'], 30 | install_requires=['django>=1.2', 'djangotoolbox'], 31 | 32 | packages=find_packages(exclude=['testproject']), 33 | include_package_data=True, 34 | classifiers=CLASSIFIERS, 35 | ) 36 | -------------------------------------------------------------------------------- /testproject/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MirkoRossini/django-redis-engine/fc6fddbcf080b0652efb728314e67b21d4b8186a/testproject/__init__.py -------------------------------------------------------------------------------- /testproject/dbindexes.py: -------------------------------------------------------------------------------- 1 | # dbindexes.py: 2 | import dbindexer 3 | dbindexer.autodiscover() 4 | -------------------------------------------------------------------------------- /testproject/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | try: 4 | import settings # Assumed to be in the same directory. 5 | except ImportError: 6 | import sys 7 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 8 | sys.exit(1) 9 | 10 | if __name__ == "__main__": 11 | execute_manager(settings) 12 | -------------------------------------------------------------------------------- /testproject/settings.py: -------------------------------------------------------------------------------- 1 | # Initialize App Engine and import the default settings (DB backend, etc.). 2 | # If you want to use a different backend you have to remove all occurences 3 | # of "djangoappengine" from this file. 4 | #from djangoappengine.settings_base import * 5 | import django.conf.global_settings as DEFAULT_SETTINGS 6 | import os 7 | 8 | SECRET_KEY = '=r-$b*8hglm+858&9t043hlm6-&6-3d3vfc4((7yd0dbrakhvi' 9 | 10 | DATABASES = { 11 | 'default': { 12 | 'ENGINE': 'django_redis_engine', 13 | 'NAME': '', 14 | 'USER': '', 15 | 'PASSWORD': '', 16 | 'HOST': 'localhost', 17 | 'PORT': 6379, 18 | 19 | } 20 | } 21 | 22 | 23 | DBINDEXER_SITECONF = 'dbindexes' 24 | 25 | DBINDEXER_BACKENDS = ( 26 | 'dbindexer.backends.FKNullFix', 27 | 'dbindexer.backends.BaseResolver', 28 | 'dbindexer.backends.InMemoryJOINResolver', 29 | ) 30 | 31 | 32 | 33 | INSTALLED_APPS = ( 34 | 'django.contrib.auth', 35 | 'django.contrib.contenttypes', 36 | 'django.contrib.sessions', 37 | 'djangotoolbox', 38 | 'testapp', 39 | 40 | ) 41 | 42 | # This test runner captures stdout and associates tracebacks with their 43 | # corresponding output. Helps a lot with print-debugging. 44 | TEST_RUNNER = 'djangotoolbox.test.CapturingTestSuiteRunner' 45 | 46 | TIME_ZONE = 'America/Chicago' 47 | 48 | LANGUAGE_CODE = 'en-us' 49 | 50 | ADMIN_MEDIA_PREFIX = '/media/admin/' 51 | 52 | 53 | MEDIA_URL = '/media/' 54 | TEMPLATE_DIRS = ( 55 | os.path.join(os.path.dirname(__file__), 'templates'), 56 | os.path.join(os.path.dirname(__file__), 'unimia/templates'), 57 | 58 | ) 59 | 60 | 61 | REDIS_SETTINGS_MODULES = ('testapp.redis_settings',) 62 | 63 | 64 | ROOT_URLCONF = 'urls' 65 | 66 | SITE_ID = 1 67 | 68 | #GAE_SETTINGS_MODULES = ( 69 | # 'unimia.gae_settings', 70 | #) 71 | 72 | TEMPLATE_CONTEXT_PROCESSORS = DEFAULT_SETTINGS.TEMPLATE_CONTEXT_PROCESSORS + ( 73 | 'django.core.context_processors.request', 74 | ) 75 | 76 | MIDDLEWARE_CLASSES = ( 77 | 'django.middleware.common.CommonMiddleware', 78 | 'django.contrib.sessions.middleware.SessionMiddleware', 79 | 'django.middleware.csrf.CsrfViewMiddleware', 80 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 81 | 'dbindexer.middleware.DBIndexerMiddleware', 82 | ) 83 | #) + DEFAULT_SETTINGS.MIDDLEWARE_CLASSES 84 | 85 | 86 | # Activate django-dbindexer if available 87 | try: 88 | import dbindexer 89 | DATABASES['native'] = DATABASES['default'] 90 | DATABASES['default'] = {'ENGINE': 'dbindexer', 'TARGET': 'native'} 91 | INSTALLED_APPS += ('dbindexer',) 92 | except ImportError,e: 93 | import traceback 94 | traceback.print_exc(20) 95 | 96 | 97 | DEBUG = True 98 | -------------------------------------------------------------------------------- /testproject/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %}Page not found{% endblock %} 3 | 4 | {% block content %} 5 | The page you requested could not be found. 6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /testproject/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %}Server error{% endblock %} 3 | 4 | {% block content %} 5 | There was an error while handling your request. 6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /testproject/templates/aproposito.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 4 |
5 |

A proposito di La Mia Uni

6 |

7 | 8 |

9 | 10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /testproject/templates/base.html: -------------------------------------------------------------------------------- 1 | 4 | 8 | 9 | Redis-testapp 10 | 11 | 12 | 13 | 14 |
15 | 16 | {% block content %}{% endblock %} 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /testproject/templates/come_aiutare.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 4 |
5 |

Come aiutare

6 |

7 | 8 |

9 | 10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /testproject/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | {% endblock %} 4 | -------------------------------------------------------------------------------- /testproject/templates/login.html: -------------------------------------------------------------------------------- 1 | {% if user.is_authenticated %} 2 | {# display something else here... #} 3 | {% else %} 4 |
5 | {% csrf_token %} 6 | Username: 7 | Password: 8 | 9 |
10 | {% endif %} 11 | 12 | -------------------------------------------------------------------------------- /testproject/templates/menu.html: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /testproject/templates/pagination.html: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /testproject/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block content %} 5 | 6 | {% if form.errors %} 7 |

Your username and password didn't match. Please try again.

8 | {% endif %} 9 | 10 |
11 | {% csrf_token %} 12 | {{ form.username.label_tag }} 13 | {{ form.username }} 14 | {{ form.password.label_tag }} 15 | {{ form.password }} 16 | 17 | 18 | 19 |
20 | 21 | {% endblock %} 22 | 23 | -------------------------------------------------------------------------------- /testproject/testapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MirkoRossini/django-redis-engine/fc6fddbcf080b0652efb728314e67b21d4b8186a/testproject/testapp/__init__.py -------------------------------------------------------------------------------- /testproject/testapp/dbindexes.py: -------------------------------------------------------------------------------- 1 | from models import Post 2 | from dbindexer.api import register_index 3 | 4 | 5 | register_index(Post, {'title': 'contains',}) 6 | -------------------------------------------------------------------------------- /testproject/testapp/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from models import * 3 | 4 | 5 | class MyForm(forms.ModelForm): 6 | class Meta: 7 | model = Post 8 | 9 | 10 | class AnswerForm(forms.ModelForm): 11 | class Meta: 12 | model = Answer 13 | exclude = ("post") 14 | 15 | -------------------------------------------------------------------------------- /testproject/testapp/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User 3 | # Create your models here. 4 | 5 | class Post(models.Model): 6 | title = models.CharField(max_length = 100) 7 | text = models.TextField() 8 | time = models.DateTimeField(auto_now_add = True,auto_now = True) 9 | 10 | class Answer(models.Model): 11 | text = models.TextField() 12 | post = models.ForeignKey(Post) 13 | -------------------------------------------------------------------------------- /testproject/testapp/redis_settings.py: -------------------------------------------------------------------------------- 1 | from models import Post 2 | from django.contrib.sessions.models import Session 3 | 4 | 5 | INDEXES = { 6 | Post: {'idxf_title_l_contains': ('startswith',), 7 | 'text':('iendswith',), 8 | 'title':('startswith','endswith'), 9 | 'time':('gt','lte'), 10 | }, 11 | Session : {'expire_date' : ('gt',) 12 | }, 13 | 14 | } 15 | 16 | -------------------------------------------------------------------------------- /testproject/testapp/templates/testapp/.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /testproject/testapp/templates/testapp/add_answer.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 | {% csrf_token %} 6 | {% include 'testapp/forms/AnswerForm.html' %} 7 | 8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /testproject/testapp/templates/testapp/add_post.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 | {% csrf_token %} 6 | {% include 'testapp/forms/MyForm.html' %} 7 | 8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /testproject/testapp/templates/testapp/forms/AnswerForm.html: -------------------------------------------------------------------------------- 1 | {{ form.non_field_errors }} 2 | 3 |
4 | {{ form.text.errors }} 5 | {{form.text.label_tag}} 6 | {{form.text}} 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /testproject/testapp/templates/testapp/forms/MyForm.html: -------------------------------------------------------------------------------- 1 | {{ form.non_field_errors }} 2 | 3 |
4 | {{ form.title.errors }} 5 | {{form.title.label_tag}} 6 | {{form.title}} 7 |
8 | 9 |
10 | {{ form.text.errors }} 11 | {{form.text.label_tag}} 12 | {{form.text}} 13 |
14 | -------------------------------------------------------------------------------- /testproject/testapp/templates/testapp/posts.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 |

Add new post

4 |

Filter by title:

5 | {% csrf_token %} 6 | 7 | 8 |
9 |

10 | {% if filter %}

Show all

{% endif %} 11 |
12 | {% for post in posts %} 13 | 14 |
15 |

{{post.title}}

16 |

{{post.text|linebreaks}}

17 |
18 |
19 | {% for answer in post.answer_set.all %} 20 | 21 |
{{answer.text|linebreaks}}
22 | 23 | {% endfor %} 24 |

Add answer

25 |
26 | 27 | {% endfor %} 28 |
29 | 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /testproject/testapp/tests.py: -------------------------------------------------------------------------------- 1 | #-*- encoding:utf-8 -*- 2 | 3 | from django.test import TestCase 4 | from models import * 5 | from md5 import md5 6 | import random 7 | 8 | from django_redis_engine import redis_transaction 9 | from django.db import transaction 10 | class SimpleTest(TestCase): 11 | 12 | def test_update_and_filters(self): 13 | """ 14 | test effects of updates on filters 15 | """ 16 | post = Post.objects.create( 17 | text = "to be updated text", 18 | title = "to be updated title" 19 | ) 20 | post.text = "updated text" 21 | post.save() 22 | posts = Post.objects.filter(text = "to be updated text") 23 | self.failUnlessEqual(len(posts), 0) 24 | posts = Post.objects.filter(text = "updated text") 25 | self.failUnlessEqual(len(posts), 1) 26 | post.title = "updated title" 27 | post.save() 28 | posts = Post.objects.filter(title__contains = "to be updated title") 29 | self.failUnlessEqual(len(posts), 0) 30 | posts = Post.objects.filter(title__contains = "updated title") 31 | self.failUnlessEqual(len(posts), 1) 32 | post.delete() 33 | 34 | def atest_insert_transaction(self): 35 | """ 36 | stress test, used to find out how much network latency 37 | affects performance using transactions. 38 | """ 39 | n = 100 40 | ng = 100 41 | ngc = 100 42 | l = [] 43 | #print redis_transaction.commit_manually(using = 'native') 44 | #print transaction.commit_manually() 45 | with redis_transaction.commit_manually(using = 'native'): 46 | print "begin" 47 | for i in range(n): 48 | tit = md5(str(random.random())+str(i)).hexdigest() 49 | l.append(tit) 50 | Post.objects.create( 51 | title = tit, 52 | text = " ".join( 53 | [md5( 54 | str(random.random())+\ 55 | str(t)+\ 56 | str(i)).hexdigest() for t in range(20)] 57 | ) 58 | ) 59 | redis_transaction.commit() 60 | for i in range(ng): 61 | p = Post.objects.get(title = l[random.randint(0,n-1)] ) 62 | for i in range(ngc): 63 | Post.objects.filter(title__contains = l[random.randint(0,n-1)]) 64 | #self.failUnlessEqual(len(posts), 1) 65 | 66 | def test_add_post_answers_and_filters(self): 67 | """ 68 | Create some posts, create answers to them, 69 | test contains filter on post title 70 | test startswith filter on post title 71 | test endswith filter on post title 72 | test iendswith filter on post text 73 | test exact filter on all fields 74 | test gt and lte filters 75 | test deletion of objects and indexes 76 | """ 77 | post1 = Post.objects.create( 78 | text = "text1", 79 | title = "title1" 80 | ) 81 | 82 | post2 = Post.objects.create( 83 | text = "text2", 84 | title = "title2" 85 | ) 86 | post3 = Post.objects.create( 87 | text = "RrRQqà", 88 | title = "AaABbB" 89 | ) 90 | answer1 = Answer.objects.create( 91 | text= "answer1 to post 1", 92 | post = post1 93 | ) 94 | answer2 = Answer.objects.create( 95 | text= "answer2 to post 1", 96 | post = post1 97 | ) 98 | answer3 = Answer.objects.create( 99 | text= "answer1 to post 2", 100 | post = post2 101 | ) 102 | posts = Post.objects.all() 103 | self.failUnlessEqual(len(posts), 3) 104 | posts = Post.objects.filter(title__contains = 'title') 105 | self.failUnlessEqual(len(posts), 2) 106 | p = Post.objects.get(title = 'title1') 107 | self.failUnlessEqual(p.pk, post1.pk) 108 | p = Post.objects.get(text = 'text2') 109 | self.failUnlessEqual(p.pk, post2.pk) 110 | a = Answer.objects.get(text = 'answer2 to post 1') 111 | self.failUnlessEqual(a.pk, answer2.pk) 112 | 113 | p.delete() 114 | posts = Post.objects.all() 115 | self.failUnlessEqual(len(posts), 2) 116 | posts = Post.objects.filter(title__contains = 'title') 117 | self.failUnlessEqual(len(posts), 1) 118 | posts = Post.objects.filter(title__startswith = 'Aa') 119 | self.failUnlessEqual(len(posts), 1) 120 | posts = Post.objects.filter(title__startswith = 'AA') 121 | self.failUnlessEqual(len(posts), 0) 122 | 123 | posts = Post.objects.filter(title__endswith = 'bB') 124 | self.failUnlessEqual(len(posts), 1) 125 | 126 | posts = Post.objects.filter(title__endswith = 'BB') 127 | self.failUnlessEqual(len(posts), 0) 128 | posts = Post.objects.filter(text__iendswith = 'qqà') 129 | self.failUnlessEqual(len(posts), 1) 130 | posts = Post.objects.filter(text__iendswith = 'QQÀ') 131 | self.failUnlessEqual(len(posts), 1) 132 | 133 | 134 | a.delete() 135 | answers = Answer.objects.filter(text = 'answer2 to post 1') 136 | self.failUnlessEqual(len(answers), 0) 137 | 138 | import datetime 139 | posts = Post.objects.filter(text__iendswith = 'QQÀ', 140 | time__gt = datetime.datetime.now() - datetime.timedelta(minutes = 10)) 141 | self.failUnlessEqual(len(posts), 1) 142 | posts = Post.objects.filter(time__gt = datetime.datetime.now() - datetime.timedelta(minutes = 10)) 143 | self.failUnlessEqual(len(posts), 2) 144 | 145 | posts = Post.objects.filter(time__lte = posts[0].time) 146 | self.failUnlessEqual(len(posts), 1) 147 | 148 | posts = Post.objects.filter(time__lte = posts[0].time + datetime.timedelta(minutes = 10)) 149 | self.failUnlessEqual(len(posts), 2) 150 | 151 | 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /testproject/testapp/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | 4 | urlpatterns = patterns('', 5 | 6 | 7 | url(r'^add_post/$', 'testapp.views.add_post', {},name = 'testapp_add_post'), 8 | 9 | url(r'^add_answer/(?P\d+)/$', 'testapp.views.add_answer', {},name = 'testapp_add_answer'), 10 | 11 | url(r'^posts/$', 'testapp.views.posts', {},name = 'testapp_posts'), 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /testproject/testapp/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | from models import Post, Answer 3 | from django.shortcuts import render_to_response 4 | from forms import * 5 | from django.template import RequestContext 6 | from django.http import HttpResponseRedirect 7 | 8 | def add_post(request,): 9 | #VIEW CODE 10 | if request.method == 'POST': 11 | form = MyForm(request.POST) 12 | if form.is_valid(): 13 | # Process the data in form.cleaned_data 14 | form.save() 15 | return HttpResponseRedirect('/posts/') 16 | else: 17 | form = MyForm() 18 | ret_dict = { 19 | 'form':form, 20 | } 21 | return render_to_response("testapp/add_post.html", 22 | ret_dict, 23 | context_instance = RequestContext(request),) 24 | 25 | 26 | 27 | def add_answer(request,post_id): 28 | #VIEW CODE 29 | if request.method == 'POST': 30 | form = AnswerForm(request.POST) 31 | post = Post.objects.get(pk = post_id) 32 | if form.is_valid(): 33 | # Process the data in form.cleaned_data 34 | form.instance.post = post 35 | form.save() 36 | return HttpResponseRedirect('/posts/') 37 | else: 38 | form = AnswerForm() 39 | ret_dict = { 40 | 'form':form, 41 | } 42 | return render_to_response("testapp/add_answer.html", 43 | ret_dict, 44 | context_instance = RequestContext(request),) 45 | 46 | 47 | 48 | def posts(request,): 49 | ret_dict = {} 50 | if request.method == 'POST' and request.POST['title_filter'] is not None and request.POST['title_filter'] != '': 51 | posts = Post.objects.filter(title__contains = request.POST['title_filter']) 52 | ret_dict['filter'] = request.POST['title_filter'] 53 | else: 54 | posts = Post.objects.all() 55 | 56 | ret_dict['posts'] = posts 57 | 58 | return render_to_response("testapp/posts.html", 59 | ret_dict, 60 | context_instance = RequestContext(request),) 61 | 62 | 63 | -------------------------------------------------------------------------------- /testproject/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | #handler500 = 'djangotoolbox.errorviews.server_error' 4 | 5 | urlpatterns = patterns('', 6 | 7 | ('', include('testapp.urls')), 8 | 9 | 10 | ) 11 | --------------------------------------------------------------------------------