-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
- Loading branch information
Showing
33 changed files
with
1,195 additions
and
1,049 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
""" | ||
SQLAlchemy patterns | ||
=================== | ||
Coaster provides a number of SQLAlchemy helper functions and mixin classes | ||
that add standard columns or special functionality. | ||
All functions and mixins are importable from the :mod:`coaster.sqlalchemy` | ||
namespace. | ||
""" | ||
|
||
from __future__ import absolute_import | ||
|
||
from .comparators import * # NOQA | ||
from .functions import * # NOQA | ||
from .roles import * # NOQA | ||
from .annotations import * # NOQA | ||
from .immutable_annotation import * # NOQA | ||
from .mixins import * # NOQA | ||
from .columns import * # NOQA |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
""" | ||
SQLAlchemy column types | ||
----------------------- | ||
""" | ||
|
||
from __future__ import absolute_import | ||
import simplejson | ||
from sqlalchemy import Column, UnicodeText | ||
from sqlalchemy.types import UserDefinedType, TypeDecorator, TEXT | ||
from sqlalchemy.orm import composite | ||
from sqlalchemy.ext.mutable import Mutable, MutableComposite | ||
from sqlalchemy_utils.types import UUIDType # NOQA | ||
from flask import Markup | ||
import six | ||
from ..gfm import markdown | ||
|
||
__all__ = ['JsonDict', 'MarkdownComposite', 'MarkdownColumn', 'UUIDType'] | ||
|
||
|
||
class JsonType(UserDefinedType): | ||
"""The PostgreSQL JSON type.""" | ||
|
||
def get_col_spec(self): | ||
return 'JSON' | ||
|
||
|
||
class JsonbType(UserDefinedType): | ||
"""The PostgreSQL JSONB type.""" | ||
|
||
def get_col_spec(self): | ||
return 'JSONB' | ||
|
||
|
||
# Adapted from http://docs.sqlalchemy.org/en/rel_0_8/orm/extensions/mutable.html#establishing-mutability-on-scalar-column-values | ||
|
||
class JsonDict(TypeDecorator): | ||
""" | ||
Represents a JSON data structure. Usage:: | ||
column = Column(JsonDict) | ||
The column will be represented to the database as a ``JSONB`` column if | ||
the server is PostgreSQL 9.4 or later, ``JSON`` if PostgreSQL 9.2 or 9.3, | ||
and ``TEXT`` for everything else. The column behaves like a JSON store | ||
regardless of the backing data type. | ||
""" | ||
|
||
impl = TEXT | ||
|
||
def load_dialect_impl(self, dialect): | ||
if dialect.name == 'postgresql': | ||
version = tuple(dialect.server_version_info[:2]) | ||
if version in [(9, 2), (9, 3)]: | ||
return dialect.type_descriptor(JsonType) | ||
elif version >= (9, 4): | ||
return dialect.type_descriptor(JsonbType) | ||
return dialect.type_descriptor(self.impl) | ||
|
||
def process_bind_param(self, value, dialect): | ||
if value is not None: | ||
value = simplejson.dumps(value, default=lambda o: six.text_type(o)) | ||
return value | ||
|
||
def process_result_value(self, value, dialect): | ||
if value is not None and isinstance(value, six.string_types): | ||
# Psycopg2 >= 2.5 will auto-decode JSON columns, so | ||
# we only attempt decoding if the value is a string. | ||
# Since this column stores dicts only, processed values | ||
# can never be strings. | ||
value = simplejson.loads(value, use_decimal=True) | ||
return value | ||
|
||
|
||
class MutableDict(Mutable, dict): | ||
@classmethod | ||
def coerce(cls, key, value): | ||
"""Convert plain dictionaries to MutableDict.""" | ||
|
||
if not isinstance(value, MutableDict): | ||
if isinstance(value, dict): | ||
return MutableDict(value) | ||
elif isinstance(value, six.string_types): | ||
# Assume JSON string | ||
if value: | ||
return MutableDict(simplejson.loads(value, use_decimal=True)) | ||
else: | ||
return MutableDict() # Empty value is an empty dict | ||
|
||
# this call will raise ValueError | ||
return Mutable.coerce(key, value) | ||
else: | ||
return value | ||
|
||
def __setitem__(self, key, value): | ||
"""Detect dictionary set events and emit change events.""" | ||
|
||
dict.__setitem__(self, key, value) | ||
self.changed() | ||
|
||
def __delitem__(self, key): | ||
"""Detect dictionary del events and emit change events.""" | ||
|
||
dict.__delitem__(self, key) | ||
self.changed() | ||
|
||
MutableDict.associate_with(JsonDict) | ||
|
||
|
||
@six.python_2_unicode_compatible | ||
class MarkdownComposite(MutableComposite): | ||
""" | ||
Represents GitHub-flavoured Markdown text and rendered HTML as a composite column. | ||
""" | ||
def __init__(self, text, html=None): | ||
if html is None: | ||
self.text = text # This will regenerate HTML | ||
else: | ||
object.__setattr__(self, 'text', text) | ||
object.__setattr__(self, '_html', html) | ||
|
||
# If the text value is set, regenerate HTML, then notify parents of the change | ||
def __setattr__(self, key, value): | ||
if key == 'text': | ||
object.__setattr__(self, '_html', markdown(value)) | ||
object.__setattr__(self, key, value) | ||
self.changed() | ||
|
||
# Return column values for SQLAlchemy to insert into the database | ||
def __composite_values__(self): | ||
return (self.text, self._html) | ||
|
||
# Return a string representation of the text (see class decorator) | ||
def __str__(self): | ||
return six.text_type(self.text) | ||
|
||
# Return a HTML representation of the text | ||
def __html__(self): | ||
return self._html or u'' | ||
|
||
# Return a Markup string of the HTML | ||
@property | ||
def html(self): | ||
return Markup(self._html or u'') | ||
|
||
# Compare text value | ||
def __eq__(self, other): | ||
return (self.text == other.text) if isinstance(other, MarkdownComposite) else (self.text == other) | ||
|
||
def __ne__(self, other): | ||
return not self.__eq__(other) | ||
|
||
# Return state for pickling | ||
def __getstate__(self): | ||
return (self.text, self._html) | ||
|
||
# Set state from pickle | ||
def __setstate__(self, state): | ||
object.__setattr__(self, 'text', state[0]) | ||
object.__setattr__(self, '_html', state[1]) | ||
self.changed() | ||
|
||
def __bool__(self): | ||
return bool(self.text) | ||
|
||
__nonzero__ = __bool__ | ||
|
||
# Allow a composite column to be assigned a string value | ||
@classmethod | ||
def coerce(cls, key, value): | ||
return cls(value) | ||
|
||
|
||
def MarkdownColumn(name, deferred=False, group=None, **kwargs): | ||
""" | ||
Create a composite column that autogenerates HTML from Markdown text, | ||
storing data in db columns named with ``_html`` and ``_text`` prefixes. | ||
""" | ||
return composite(MarkdownComposite, | ||
Column(name + '_text', UnicodeText, **kwargs), | ||
Column(name + '_html', UnicodeText, **kwargs), | ||
deferred=deferred, group=group or name | ||
) |
Oops, something went wrong.