public
Description: RESTful Blog for Google App Engine
Homepage:
Clone URL: git://github.com/DocSavage/bloog.git
bloog / models / blog.py
100644 197 lines (168 sloc) 7.527 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# The MIT License
#
# Copyright (c) 2008 William T. Katz
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
 
import logging
 
from google.appengine.api import memcache
from google.appengine.ext import db
 
import config
import models
from models import search
 
# Handle generation of thread strings
def get_thread_string(article, cur_thread_string):
    min_str = cur_thread_string + '000'
    max_str = cur_thread_string + '999'
    q = db.GqlQuery("SELECT * FROM Comment " +
                    "WHERE article = :1 " +
                    "AND thread >= :2 AND thread <= :3",
                    article, min_str, max_str)
    num_comments = q.count(999)
    if num_comments > 998:
        return None # Only allow 999 comments on each tree level
    return cur_thread_string + "%03d" % (num_comments + 1)
 
class Article(search.SearchableModel):
    unsearchable_properties = ['permalink', 'legacy_id', 'article_type',
                               'excerpt', 'html', 'format', 'tag_keys']
    json_does_not_include = ['assoc_dict']
 
    permalink = db.StringProperty(required=True)
    # Useful for aliasing of old urls
    legacy_id = db.StringProperty()
    title = db.StringProperty(required=True)
    article_type = db.StringProperty(required=True,
                                     choices=set(["article", "blog entry"]))
    # Body can be in any format supported by Bloog (e.g. textile)
    body = db.TextProperty(required=True)
    # If available, we use 'excerpt' to summarize instead of
    # extracting the first 68 words of 'body'.
    excerpt = db.TextProperty()
    # The html property is generated from body
    html = db.TextProperty()
    published = db.DateTimeProperty(auto_now_add=True)
    updated = db.DateTimeProperty(auto_now_add=True)
    format = db.StringProperty(required=True,
                               choices=set(["html", "textile",
                                            "markdown", "text"]))
    # Picked dict for sidelinks, associated Amazon items, etc.
    assoc_dict = db.BlobProperty()
    # To prevent full query when just showing article headlines
    num_comments = db.IntegerProperty(default=0)
    # Use keys instead of db.Category for consolidation of tag names
    tags = db.StringListProperty(default=[])
    tag_keys = db.ListProperty(db.Key, default=[])
    two_columns = db.BooleanProperty()
    allow_comments = db.BooleanProperty()
    # A list of languages for code embedded in article.
    # This lets us choose the proper javascript for pretty viewing.
    embedded_code = db.StringListProperty()
 
    def get_comments(self):
        """Return comments lexicographically sorted on thread string"""
        q = db.GqlQuery("SELECT * FROM Comment " +
                        "WHERE article = :1 " +
                        "ORDER BY thread ASC", self.key())
        return [comment for comment in q]
    comments = property(get_comments) # No set for now
 
    def set_associated_data(self, data):
        """
Serialize data that we'd like to store with this article.
Examples include relevant (per article) links and associated
Amazon items.
"""
        import pickle
        self.assoc_dict = pickle.dumps(data)
 
    def get_associated_data(self):
        import pickle
        return pickle.loads(self.assoc_dict)
 
    def full_permalink(self):
        return config.BLOG['root_url'] + '/' + self.permalink
    
    def rfc3339_published(self):
        return self.published.strftime('%Y-%m-%dT%H:%M:%SZ')
 
    def rfc3339_updated(self):
        return self.updated.strftime('%Y-%m-%dT%H:%M:%SZ')
 
    def is_big(self):
        guess_chars = len(self.html) + self.num_comments * 80
        if guess_chars > 2000 or \
           self.embedded_code or \
           '<img' in self.html or \
           '<code>' in self.html or \
           '<pre>' in self.html:
            return True
        else:
            return False
 
    def next_comment_thread_string(self):
        'Returns thread string for next comment for this article'
        return get_thread_string(self, '')
 
    def to_atom_xml(self):
        """Returns a string suitable for inclusion in Atom XML feed
Internal html property should already have XHTML entities
converted into unicode. However, ampersands are valid ASCII
and will cause issues with XML, so reconvert ampersands to
valid XML entities &amp;
"""
        import re
        return re.sub('&(?!amp;)', '&amp;', self.html)
 
class Comment(models.SerializableModel):
    """Stores comments and their position in comment threads.
 
Thread string describes the tree using 3 digit numbers.
This allows lexicographical sorting to order comments
and easy indentation computation based on the string depth.
Example for comments that are nested except first response:
001
001.001
001.002
001.002.001
001.002.001.001
NOTE: This means we assume less than 999 comments in
response to a parent comment, and we won't have
nesting that causes our thread string > 500 bytes.
TODO -- Put in error checks
"""
    name = db.StringProperty()
    email = db.EmailProperty()
    homepage = db.StringProperty()
    title = db.StringProperty()
    body = db.TextProperty(required=True)
    published = db.DateTimeProperty(auto_now_add=True)
    article = db.ReferenceProperty(Article)
    thread = db.StringProperty(required=True)
 
    def get_indentation(self):
        # Indentation is based on degree of nesting in "thread"
        nesting_str_array = self.thread.split('.')
        return min([len(nesting_str_array), 10])
 
    def next_child_thread_string(self):
        'Returns thread string for next child of this comment'
        return get_thread_string(self.article, self.thread + '.')
 
 
class Tag(models.MemcachedModel):
    # Inserts these values into aggregate list returned by Tag.list()
    list_includes = ['counter.count', 'name']
 
    def delete(self):
        self.delete_counter()
        super(Tag, self).delete()
 
    def get_counter(self):
        counter = models.Counter('Tag' + self.name)
        return counter
 
    def set_counter(self, value):
        # Not implemented at this time
        pass
 
    def delete_counter(self):
        models.Counter('Tag' + self.name).delete()
 
    counter = property(get_counter, set_counter, delete_counter)
 
    def get_name(self):
        return self.key().name()
    name = property(get_name)