Commits

Constantin Veretennicov committed e1fe1df

* Keeping copy of app settings in memcache to reduce number of datastore calls.
* Announce url is now overlayed over dialog screenshot and depends on the host name instead of being part of static image.
* Added simple /sitemap.txt handler.

Comments (0)

Files changed (9)

 
 class AdminHandler(webapp.RequestHandler):
   def get(self):
+    setting_atributes = [
+      (name, description, default == '\n')
+      for (name, (description, default, _))
+      in sorted(settings.meta.iteritems(), key=lambda x: x[1][0])]
+    setting_values = settings.get_values(*[a[0] for a in setting_atributes])
     template_values = {
       'settings': [
-        {'name': name,
-         'label': description,
-         'value': settings.get_value(name),
-         'multiline': (default == '\n')}
-        for (name, (description, default, _))
-        in sorted(settings.meta.iteritems(), key=lambda x: x[1][0])]}
+        {'name': name, 'label': label, 'multiline': multiline, 'value': value}
+        for ((name, label, multiline), value)
+        in zip(setting_atributes, setting_values)]}
     path = os.path.join(os.path.dirname(__file__), 'templates', 'admin.html')
     self.response.out.write(template.render(path, template_values))
 
 application: tracko
-version: 7
+version: 8
 runtime: python
 api_version: 1
 
   def get(self):
     template_values = {}
     try:
-      template_values['ga_token'] = settings.get_value('ga_token')
+      template_values['ga_token'] = settings.get_values('ga_token')[0]
     except:
       logging.error('Failed to retrieve ga_token from settings.')
     try:
-      template_values['ad_block'] = settings.get_value('ad_block').strip()
+      template_values['ad_block'] = settings.get_values('ad_block')[0].strip()
     except:
       logging.error('Failed to retrieve ad_block from settings.')
     template_values['announce_url'] = 'http://%s%s/announce' % (
     self.response.headers['content-type'] = 'text/plain'
     self.response.out.write(response_body)
 
+class SitemapHandler(webapp.RequestHandler):
+  def get(self):
+    self.response.headers['content-type'] = 'text/plain'
+    self.response.out.write(
+      'http://%s%s/\n' % (
+        self.request.server_name,
+        ':%d' % self.request.server_port \
+          if self.request.server_port != 80 else ''))
 
 def create_application():
   return webapp.WSGIApplication(
     [('/', MainHandler),
-     ('/announce', AnnounceHandler)],
+     ('/announce', AnnounceHandler),
+     ('/sitemap.txt', SitemapHandler),],
     debug=True)
 
 def main():
-import models
+import logging, models
+from google.appengine.api import memcache
 
 meta = {
   # name -> (label, default, parser-func)
   'ad_block': (
     'Ad block (HTML literal)', '\n', str),}
 
-def get_value(name):
-  setting_meta = meta.get(name)
-  if setting_meta is None:
-    raise KeyError('No such setting: %s' % (name,))
-  settings = models.AppSettings.get_or_insert(key_name='singleton')
-  return getattr(settings, name, setting_meta[1])
 
 def get_values(*names):
-  result = []
-  settings = models.AppSettings.get_or_insert(key_name='singleton')
   for name in names:
     setting_meta = meta.get(name)
     if setting_meta is None:
       raise KeyError('No such setting: %s' % (name,))
-    result.append(getattr(settings, name, setting_meta[1]))
+  result = []
+  cached_settings = memcache.get_multi(names, key_prefix='appsettings.')
+  stored_settings = None
+  settings_to_cache = {}
+  for name in names:
+    setting_value = cached_settings.get(name)
+    if setting_value is None: # read missing settings from datastore
+      if stored_settings is None:
+        stored_settings = models.AppSettings.get_or_insert(key_name='singleton')
+      setting_meta = meta.get(name)
+      setting_value = getattr(stored_settings, name, setting_meta[1])
+      settings_to_cache[name] = setting_value
+    result.append(setting_value)
+  if settings_to_cache:
+    memcache.set_multi(settings_to_cache, key_prefix='appsettings.')
   return result
 
 def set_values(**names_and_values):
+  logging.info('settings.set_values: %r', names_and_values)
   settings = models.AppSettings.get_or_insert(key_name='singleton')
   for name, value in names_and_values.iteritems():
     setting_meta = meta.get(name)
     if setting_meta is None:
       raise KeyError('No such setting: %s' % (name,))
-    setattr(settings, name, value)
+    setting_ctor = setting_meta[2]
+    converted_value = setting_ctor(value) # check if value is convertible
+    setattr(settings, name, converted_value)
+  memcache.set_multi(names_and_values, key_prefix='appsettings.')
   settings.put()

app/static/site.css

   text-decoration: none;
 }
 
-#ad_block {
+#ad-block {
   padding-left: 1em;  
 }

app/static/utorrent-instructions.png

Old
Old image
New
New image

app/templates/index.html

     #right-column {
       margin: 1em 0;
       float: left;
+      background-image: url(static/utorrent-instructions.png?v=3);
+      min-width: 440px;
+      min-height: 489px;
+      font-size: 8pt;
+      font-family: Tahoma, sans-serif;
+    }
+    #announce-url-overlay {
+      position: relative;
+      top: 210px;
+      left: 100px;
+      max-width: 307px;
+      padding: 2.4pt 4pt;
     }
   </style>
 {% endblock %}
         <ol>
           <li>Select "File | Create New Torrent..." from menu</li>
           <li>Choose a file or directory to share</li>
-          <li>Specify <strong>{{ announce_url }}</strong>
+          <li>Specify <strong>{{ announce_url|escape }}</strong>
               as tracker</li>
           <li>Save your .torrent file</li>
           <li>Send the .torrent file to other people via email,
           >http://bitbucket.org/kveretennicov/tracko/</a>
       </div>
       {% if ad_block %}
-        <div id="ad_block">
+        <div id="ad-block">
           {{ ad_block }}
         </div>
       {% endif %}
     </div>
-    <img
-      id="right-column"
-      src="static/utorrent-instructions.png?v=2"
-      alt="&micro;Torrent's &quot;Create New Torrent&quot; dialog"
-      width="440" height="489">
+    <div id="right-column">
+      <div id="announce-url-overlay">
+        {{ announce_url|escape }}
+      </div>
+    </div>
     <br style="clear: both">
   </div>
 

tests/test_settings.py

 
 def setup():
   util.clear_datastore()
+  util.stub_memcache()
   assert models.Torrent.all().count() == 0
   assert models.TorrentPeer.all().count() == 0
 
 
 
 @with_setup(util.clear_datastore)
-def test_raise_on_unknown_setting_value():
-  assert_raises(KeyError, settings.get_value, 'spam')
+def test_raise_on_unknown_setting_name():
   assert_raises(KeyError, settings.get_values, 'ga_token', 'spam')
   assert_raises(KeyError, settings.set_values, ga_token='eggs', spam='bacon')
 
 
+def test_raise_error_on_setting_value_of_wrong_type():
+  assert_raises(
+    ValueError, settings.set_values, recommended_announce_interval='spam')
+
+
 @with_setup(util.clear_datastore)
 def test_default_setting_values():
 
   def check_setting_value(setting_name, expected_value):
-    assert_equals(settings.get_value(setting_name), expected_value)
+    assert_equals(
+      settings.get_values(setting_name), [expected_value])
     assert_equals(
       settings.get_values(setting_name, 'ga_token'),
-      [expected_value, settings.get_value('ga_token')])
+      [expected_value, settings.get_values('ga_token')[0]])
 
   for setting_name, (_, default_value, _) in settings.meta.iteritems():
       yield check_setting_value, setting_name, default_value
 
 def test_settings_can_be_changed():
 
-  def check_setting_value(setting_name):
-    assert_not_equals(settings.get_value(setting_name), setting_name * 10)
-    settings.set_values(**{setting_name: setting_name * 10})
-    assert_equals(settings.get_value(setting_name), setting_name * 10)
+  def check_setting_value(setting_name, ctor):
+    setting_value = 42 if ctor is int else setting_name * 10
+    assert_not_equals(settings.get_values(setting_name), [setting_value])
+    settings.set_values(**{setting_name: setting_value})
+    assert_equals(settings.get_values(setting_name), [setting_value])
 
   for setting_name in settings.meta:
-      yield check_setting_value, setting_name
+      yield check_setting_value, setting_name, settings.meta[setting_name][2]
 
 
 @with_setup(util.clear_datastore)
+@with_setup(util.clear_memcache)
 def test_recommended_announce_interval_setting_is_respected():
   
   settings.set_values(recommended_announce_interval=12345)
 
 
 @with_setup(util.clear_datastore)
+@with_setup(util.clear_memcache)
 def test_n_max_peers_to_return_setting_is_respected():
 
   for i in range(10):
 
 
 @with_setup(util.clear_datastore)
+@with_setup(util.clear_memcache)
 def test_n_default_peers_to_return_setting_is_respected():
 
   for i in range(10):
 from google.appengine.api import apiproxy_stub_map
 from google.appengine.api import datastore_file_stub
+from google.appengine.api.memcache import memcache_stub
 
 
 def clear_datastore():
-    # Use a fresh stub datastore.
-    apiproxy_stub_map.apiproxy = apiproxy_stub_map.APIProxyStubMap()
-    stub = datastore_file_stub.DatastoreFileStub('tracko', '/dev/null', '/dev/null')
-    apiproxy_stub_map.apiproxy.RegisterStub('datastore_v3', stub)
+  # use a fresh stub datastore.
+  stub = datastore_file_stub.DatastoreFileStub('tracko', '/dev/null', '/dev/null')
+  apiproxy_stub_map.apiproxy._APIProxyStubMap__stub_map['datastore_v3'] = stub
+
+def stub_memcache():
+  # use a fresh memcache stub.
+  stub = memcache_stub.MemcacheServiceStub()
+  apiproxy_stub_map.apiproxy._APIProxyStubMap__stub_map['memcache'] = stub
+
+from google.appengine.api.memcache import flush_all as clear_memcache