Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge branch 'mdn' into master-mdn-merge

Conflicts:
	Vagrantfile
	apps/actioncounters/migrations/__init__.py
	apps/contentflagging/migrations/__init__.py
	apps/devmo/tests/test_views.py
	apps/taggit_extras/__init__.py
	apps/taggit_extras/tests/__init__.py
	configs/htaccess
	lib/taggit_extras/__init__.py
	lib/taggit_extras/tests/__init__.py
	puppet/files/etc/httpd/conf.d/mozilla-kuma-apache.conf
	puppet/files/vagrant/settings_local.py
	puppet/manifests/classes/site-config.pp
	scripts/build.sh
	scripts/update_site.py
	templates/403.html
  • Loading branch information...
commit 6b0bc8eb89a3d3ded6a4836215bbf83b6ab4d13c 2 parents b2f4a68 + 7e76a7b
Les Orchard lmorchard authored
Showing with 4,604 additions and 974 deletions.
  1. +4 −13 README-vagrant.md
  2. +12 −0 apps/actioncounters/cron.py
  3. +116 −0 apps/actioncounters/migrations/0001_initial.py
  4. +92 −0 apps/actioncounters/migrations/0002_unique_hash_index.py
  5. +110 −0 apps/actioncounters/migrations/0003_update_unique_hashes.py
  6. 0  apps/actioncounters/migrations/__init__.py
  7. +30 −37 apps/actioncounters/models.py
  8. +23 −15 apps/actioncounters/tests.py
  9. +20 −16 apps/actioncounters/utils.py
  10. +95 −0 apps/contentflagging/migrations/0001_initial.py
  11. +91 −0 apps/contentflagging/migrations/0002_unique_hash_index.py
  12. +87 −0 apps/contentflagging/migrations/0003_update_unique_hashes.py
  13. 0  apps/contentflagging/migrations/__init__.py
  14. +24 −22 apps/contentflagging/models.py
  15. +18 −12 apps/contentflagging/tests.py
  16. +19 −17 apps/contentflagging/utils.py
  17. +19 −24 apps/demos/__init__.py
  18. +8 −5 apps/demos/forms.py
  19. +6 −5 apps/demos/helpers.py
  20. +2 −0  apps/demos/templates/demos/detail.html
  21. +77 −36 apps/demos/templates/demos/devderby_landing.html
  22. +1 −1  apps/demos/templates/demos/elements/profile_link.html
  23. +1 −1  apps/demos/templates/demos/elements/submission_creator.html
  24. +7 −4 apps/demos/templates/demos/elements/submission_listing.html
  25. +6 −6 apps/demos/tests/test_views.py
  26. +28 −34 apps/demos/views.py
  27. +2 −0  apps/devmo/__init__.py
  28. +3 −3 apps/devmo/fixtures/Mozillapeopleevents.csv
  29. +1 −1  apps/devmo/fixtures/bad_date.csv
  30. 0  apps/devmo/fixtures/{initial_data.json → devmo_calendar.json}
  31. +1 −0  apps/devmo/fixtures/user_docs_activity_feed.xml
  32. +1 −1  apps/devmo/fixtures/xss.csv
  33. +16 −4 apps/devmo/forms.py
  34. +113 −0 apps/devmo/migrations/0009_auto__chg_field_event_description__chg_field_event_people.py
  35. +108 −0 apps/devmo/migrations/0010_irc_nickname_in_profile.py
  36. +81 −29 apps/devmo/models.py
  37. +10 −10 apps/devmo/templates/devmo/calendar.html
  38. +42 −193 apps/devmo/templates/devmo/profile.html
  39. +14 −47 apps/devmo/templates/devmo/profile_edit.html
  40. +5 −0 apps/devmo/tests/test_misc.py
  41. +24 −5 apps/devmo/tests/test_models.py
  42. +69 −3 apps/devmo/tests/test_views.py
  43. +23 −5 apps/devmo/views.py
  44. +1 −0  apps/feeder/fixtures/test_data.json
  45. +6 −0 apps/landing/templates/landing/addons.html
  46. +47 −0 apps/landing/templates/landing/discussion.html
  47. +15 −0 apps/landing/templates/landing/forum_archive.html
  48. +31 −19 apps/landing/templates/landing/home.html
  49. +1 −5 apps/landing/templates/landing/learn.html
  50. +1 −5 apps/landing/templates/landing/learn_css.html
  51. +5 −0 apps/landing/templates/landing/learn_fineprint.html
  52. +1 −5 apps/landing/templates/landing/learn_html.html
  53. +1 −6 apps/landing/templates/landing/learn_javascript.html
  54. +8 −1 apps/landing/templates/landing/mobile.html
  55. +7 −1 apps/landing/templates/landing/mozilla.html
  56. +2 −2 apps/landing/templates/landing/promote_buttons.html
  57. +7 −1 apps/landing/templates/landing/web.html
  58. +100 −0 apps/landing/test_views.py
  59. +2 −0  apps/landing/urls.py
  60. +8 −0 apps/landing/views.py
  61. +2 −2 apps/sumo/helpers.py
  62. +80 −0 apps/taggit_extras/utils.py
  63. +0 −3  media/css/demos.css
  64. +4 −2 media/css/devderby.css
  65. +36 −12 media/css/mdn-screen.css
  66. +6 −6 media/css/promote.css
  67. BIN  media/img/balloons.png
  68. BIN  media/img/beast-403.png
  69. BIN  media/img/beast-404.png
  70. BIN  media/img/beast-500.png
  71. BIN  media/img/devderby/judges/benward.jpg
  72. BIN  media/img/devderby/judges/chriswanstrath.jpg
  73. BIN  media/img/devderby/judges/jeffmalkin.jpg
  74. BIN  media/img/devderby/judges/mikedavies.jpg
  75. BIN  media/img/devderby/judges/ryangrove.jpg
  76. BIN  media/img/webfwd-promo-banner.png
  77. BIN  media/img/webfwd-promo-square.png
  78. +206 −0 media/js/libs/jquery.gmap-1.1.0.js
  79. +22 −31 media/js/mdn/calendar.js
  80. +0 −179 media/js/mdn/geocode.js
  81. +130 −0 media/js/mdn/profile.js
  82. +12 −3 media/template/profile-edit-blank.php
  83. +42 −19 media/template/profile-edit.php
  84. +9 −0 media/template/profile-partial.php
  85. +28 −19 media/template/profile.php
  86. +4 −0 migrations/09-contentflagging-convert-to-south.sql
  87. +4 −0 migrations/10-actioncounters-convert-to-south.sql
  88. +35 −0 migrations/south/constance/0001_initial.py
  89. 0  migrations/south/constance/__init__.py
  90. +1 −0  puppet/files/etc/httpd/conf.d/mozilla-kuma-apache.conf
  91. +6 −0 puppet/files/tmp/init.sql
  92. +2,000 −0 puppet/files/tmp/phpbb.sql
  93. +7 −10 puppet/files/vagrant/settings_local.py
  94. +17 −0 puppet/files/var/www/phpbb/config.php
  95. +74 −0 puppet/manifests/classes/phpbb.pp
  96. +8 −6 puppet/manifests/dev-vagrant-mdn.pp
  97. +13 −0 puppet/manifests/mozilla_kuma.pp
  98. +2 −2 scripts/build.sh
  99. +8 −3 scripts/update_site.py
  100. +49 −4 settings.py
  101. +47 −29 templates/403.html
  102. +24 −8 templates/404.html
  103. +25 −9 templates/500.html
  104. +3 −3 templates/base.html
  105. +47 −29 templates/handlers/403.html
  106. +1 −1  templates/includes/login.html
17 README-vagrant.md
View
@@ -19,13 +19,9 @@ reasons.
sudo gem update
sudo gem install vagrant
- # Clone a Kuma repo, switch to my vagrant branch (for now)
- git clone git://github.com/mozilla/kuma.git
- cd kuma
-
# Fire up the VM and install everything, go take a bike ride (approx. 15 min)
# Clone a Kuma repo, switch to "mdn" branch (for now)
- git clone git://github.com/lmorchard/kuma.git
+ git clone git://github.com/mozilla/kuma.git
cd kuma
git submodule update --init --recursive
@@ -35,17 +31,12 @@ reasons.
# If the process fails with an error, try running the Puppet setup again.
# (Not sure why yet, but usually this just works.)
vagrant provision
-
- # Optional: Download and import data extracted from the production site
- # This can take a long while, since there's over 500MB of data
- vagrant ssh
- sudo puppet apply /vagrant/puppet/manifests/dev-vagrant-mdn-import.pp
- # Add dev-kuma.developer.mozilla.org to /etc/hosts
- echo '192.168.10.50 dev-kuma.developer.mozilla.org' >> /etc/hosts
+ # Add developer-kumadev.mozilla.org to /etc/hosts
+ echo '192.168.10.50 developer-kumadev.mozilla.org' >> /etc/hosts
# Everything should be working now.
- curl 'http://dev-kuma.developer.mozilla.org'
+ curl 'http://developer-kumadev.mozilla.org'
# Edit files as usual on your host machine; the current directory is
# mounted via NFS at /vagrant within the VM.
12 apps/actioncounters/cron.py
View
@@ -5,6 +5,7 @@
import cronjobs
# TODO: Figure out a way to do this per-class? Would need to break up some of the SQL calls.
+ACTIONCOUNTERS_ANON_GC_WINDOW = getattr(settings, "ACTIONCOUNTERS_ANON_GC_WINDOW", "2 MONTH")
ACTIONCOUNTERS_RECENT_COUNT_WINDOW = getattr(settings, "ACTIONCOUNTERS_RECENT_COUNT_WINDOW", "14 DAY")
@cronjobs.register
@@ -27,6 +28,17 @@ def get_update(ct_pk, obj_pk):
cursor = connection.cursor()
+ # Garbage collect any counters for anonymous users over a certain age.
+ cursor.execute("""
+ DELETE
+ FROM actioncounters_actioncounterunique
+ WHERE
+ user_id IS NULL AND
+ modified < date_sub(now(), INTERVAL %(interval)s)
+ """ % dict(
+ interval=ACTIONCOUNTERS_ANON_GC_WINDOW
+ ))
+
# Any counters too old for the window should be set to 0
cursor.execute("""
SELECT content_type_id, object_pk, name
116 apps/actioncounters/migrations/0001_initial.py
View
@@ -0,0 +1,116 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding model 'TestModel'
+ db.create_table('actioncounters_testmodel', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('title', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)),
+ ('views_total', self.gf('django.db.models.fields.IntegerField')(default=0, db_index=True, blank=True)),
+ ('views_recent', self.gf('django.db.models.fields.IntegerField')(default=0, db_index=True, blank=True)),
+ ('boogs_total', self.gf('django.db.models.fields.IntegerField')(default=0, db_index=True, blank=True)),
+ ('boogs_recent', self.gf('django.db.models.fields.IntegerField')(default=0, db_index=True, blank=True)),
+ ('likes_total', self.gf('django.db.models.fields.IntegerField')(default=0, db_index=True, blank=True)),
+ ('likes_recent', self.gf('django.db.models.fields.IntegerField')(default=0, db_index=True, blank=True)),
+ ('frobs_total', self.gf('django.db.models.fields.IntegerField')(default=0, db_index=True, blank=True)),
+ ('frobs_recent', self.gf('django.db.models.fields.IntegerField')(default=0, db_index=True, blank=True)),
+ ))
+ db.send_create_signal('actioncounters', ['TestModel'])
+
+ # Adding model 'ActionCounterUnique'
+ db.create_table('actioncounters_actioncounterunique', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(related_name='content_type_set_for_actioncounterunique', to=orm['contenttypes.ContentType'])),
+ ('object_pk', self.gf('django.db.models.fields.CharField')(max_length=32)),
+ ('name', self.gf('django.db.models.fields.CharField')(max_length=64, db_index=True)),
+ ('total', self.gf('django.db.models.fields.IntegerField')()),
+ ('ip', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=40, null=True, blank=True)),
+ ('session_key', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=40, null=True, blank=True)),
+ ('user_agent', self.gf('django.db.models.fields.CharField')(db_index=True, max_length=128, null=True, blank=True)),
+ ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)),
+ ('modified', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
+ ))
+ db.send_create_signal('actioncounters', ['ActionCounterUnique'])
+
+
+ def backwards(self, orm):
+
+ # Deleting model 'TestModel'
+ db.delete_table('actioncounters_testmodel')
+
+ # Deleting model 'ActionCounterUnique'
+ db.delete_table('actioncounters_actioncounterunique')
+
+
+ models = {
+ 'actioncounters.actioncounterunique': {
+ 'Meta': {'object_name': 'ActionCounterUnique'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_actioncounterunique'", 'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '40', 'null': 'True', 'blank': 'True'}),
+ 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'object_pk': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '40', 'null': 'True', 'blank': 'True'}),
+ 'total': ('django.db.models.fields.IntegerField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'user_agent': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '128', 'null': 'True', 'blank': 'True'})
+ },
+ 'actioncounters.testmodel': {
+ 'Meta': {'object_name': 'TestModel'},
+ 'boogs_recent': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'}),
+ 'boogs_total': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'}),
+ 'frobs_recent': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'}),
+ 'frobs_total': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'likes_recent': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'}),
+ 'likes_total': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'views_recent': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'}),
+ 'views_total': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'})
+ },
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ }
+ }
+
+ complete_apps = ['actioncounters']
92 apps/actioncounters/migrations/0002_unique_hash_index.py
View
@@ -0,0 +1,92 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Deleting field 'ActionCounterUnique.session_key'
+ db.delete_column('actioncounters_actioncounterunique', 'session_key')
+
+ # Adding field 'ActionCounterUnique.unique_hash'
+ db.add_column('actioncounters_actioncounterunique', 'unique_hash', self.gf('django.db.models.fields.CharField')(blank=True, max_length=32, null=True, db_index=True), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Adding field 'ActionCounterUnique.session_key'
+ db.add_column('actioncounters_actioncounterunique', 'session_key', self.gf('django.db.models.fields.CharField')(blank=True, max_length=40, null=True, db_index=True), keep_default=False)
+
+ # Deleting field 'ActionCounterUnique.unique_hash'
+ db.delete_column('actioncounters_actioncounterunique', 'unique_hash')
+
+
+ models = {
+ 'actioncounters.actioncounterunique': {
+ 'Meta': {'object_name': 'ActionCounterUnique'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_actioncounterunique'", 'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '40', 'null': 'True', 'blank': 'True'}),
+ 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'object_pk': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'total': ('django.db.models.fields.IntegerField', [], {}),
+ 'unique_hash': ('django.db.models.fields.CharField', [], {'default': "''", 'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'user_agent': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '128', 'null': 'True', 'blank': 'True'})
+ },
+ 'actioncounters.testmodel': {
+ 'Meta': {'object_name': 'TestModel'},
+ 'boogs_recent': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'}),
+ 'boogs_total': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'}),
+ 'frobs_recent': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'}),
+ 'frobs_total': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'likes_recent': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'}),
+ 'likes_total': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'views_recent': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'}),
+ 'views_total': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'})
+ },
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ }
+ }
+
+ complete_apps = ['actioncounters']
110 apps/actioncounters/migrations/0003_update_unique_hashes.py
View
@@ -0,0 +1,110 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+ "Update the unique hashes on all objects"
+
+ # First, delete older anonymous unique counters, since they're not all
+ # that useful at this point or worth migrating.
+ #
+ # This should also be done in the update_actioncounter_counts cronjob,
+ # but wasn't implemented before this migration. Doing it now, so the
+ # data migration has a smaller set to work with.
+ (orm.ActionCounterUnique.objects
+ .filter(user=None, modified__lt=datetime.datetime(2011,8,1))
+ .delete())
+
+ # Update all remaining counters with a unique hash.
+ from actioncounters.utils import get_unique
+ counters = orm.ActionCounterUnique.objects.all()
+ for counter in counters:
+ try:
+ # Need to duplicate the custom saving code from the model,
+ # since South's frozen version of it doesn't have the logic.
+ user, ip, user_agent, unique_hash = get_unique(
+ counter.content_type, counter.object_pk, counter.name,
+ ip=counter.ip, user_agent=counter.user_agent,
+ user=counter.user)
+ counter.unique_hash = unique_hash
+ counter.save()
+ except IntegrityError:
+ # If there's already a counter with the unique hash, delete
+ # this one as a duplicate.
+ counter.delete()
+
+
+ def backwards(self, orm):
+ "Nothing to reverse - the field will be deleted"
+
+
+ models = {
+ 'actioncounters.actioncounterunique': {
+ 'Meta': {'object_name': 'ActionCounterUnique'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_actioncounterunique'", 'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '40', 'null': 'True', 'blank': 'True'}),
+ 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'object_pk': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'total': ('django.db.models.fields.IntegerField', [], {}),
+ 'unique_hash': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True', 'db_index': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'user_agent': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '128', 'null': 'True', 'blank': 'True'})
+ },
+ 'actioncounters.testmodel': {
+ 'Meta': {'object_name': 'TestModel'},
+ 'boogs_recent': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'}),
+ 'boogs_total': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'}),
+ 'frobs_recent': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'}),
+ 'frobs_total': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'likes_recent': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'}),
+ 'likes_total': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'views_recent': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'}),
+ 'views_total': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_index': 'True', 'blank': 'True'})
+ },
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ }
+ }
+
+ complete_apps = ['actioncounters']
0  apps/actioncounters/migrations/__init__.py
View
No changes.
67 apps/actioncounters/models.py
View
@@ -1,6 +1,7 @@
"""Models for activity counters"""
import logging
+import hashlib
from django.db import models
from django.conf import settings
@@ -40,45 +41,25 @@ def get_unique_for_request(self, object, action_name, request, create=True):
refrain from creating a new one if the intent is just to check
existence."""
content_type = ContentType.objects.get_for_model(object)
- user, ip, user_agent, session_key = get_unique(request)
- try:
-
- if create:
- return self.get_or_create(
- content_type=content_type, object_pk=object.pk,
- name=action_name,
- ip=ip, user_agent=user_agent, user=user,
- session_key=session_key,
- defaults=dict( total=0 ))
- else:
- try:
- return (
- self.get(
- content_type=content_type, object_pk=object.pk,
- name=action_name,
- ip=ip, user_agent=user_agent, user=user,
- session_key=session_key,),
- False
- )
- except ActionCounterUnique.DoesNotExist:
- return ( None, False )
-
- except MultipleObjectsReturned:
- # HACK: There seems to be a race condition in get_or_create. :(
- # Happens very rarely, but when it does, seems like the best thing
- # to do is try to clean up?
- counters = self.filter(
- content_type=content_type, object_pk=object.pk,
- name=action_name,
- ip=ip, user_agent=user_agent, user=user,
- session_key=session_key)
- counter = counters[0]
- for c in counters[1:]: c.delete()
- return ( counter, False )
+ user, ip, user_agent, unique_hash = get_unique(content_type, object.pk,
+ action_name,
+ request=request)
+ if create:
+ return self.get_or_create(unique_hash=unique_hash,
+ defaults=dict(content_type=content_type, object_pk=object.pk,
+ name=action_name, ip=ip,
+ user_agent=user_agent, user=user,
+ total=0))
+ else:
+ try:
+ return (self.get(unique_hash=unique_hash), False)
+ except ActionCounterUnique.DoesNotExist:
+ return (None, False)
class ActionCounterUnique(models.Model):
"""Action counter for a unique request / user"""
+
objects = ActionCounterUniqueManager()
content_type = models.ForeignKey(
@@ -96,17 +77,29 @@ class ActionCounterUnique(models.Model):
ip = models.CharField(max_length=40, editable=False,
db_index=True, blank=True, null=True)
- session_key = models.CharField(max_length=40, editable=False,
- db_index=True, blank=True, null=True)
user_agent = models.CharField(max_length=128, editable=False,
db_index=True, blank=True, null=True)
user = models.ForeignKey(User, editable=False,
db_index=True, blank=True, null=True)
+ # HACK: As it turns out, MySQL doesn't consider two rows with NULL values
+ # in a column as duplicates. So, resorting to calculating a unique hash in
+ # code.
+ unique_hash = models.CharField(max_length=32, editable=False,
+ unique=True, db_index=True, null=True)
+
modified = models.DateTimeField(
_('date last modified'),
auto_now=True, blank=False)
+ def save(self, *args, **kwargs):
+ # Ensure unique_hash is updated whenever the object is saved
+ user, ip, user_agent, unique_hash = get_unique(
+ self.content_type, self.object_pk, self.name,
+ ip=self.ip, user_agent=self.user_agent, user=self.user)
+ self.unique_hash = unique_hash
+ super(ActionCounterUnique, self).save(*args, **kwargs)
+
def increment(self, min=0, max=1):
return self._change_total(1, min, max)
38 apps/actioncounters/tests.py
View
@@ -47,39 +47,47 @@ def tearDown(self):
# logging.debug("SQL %s" % sql)
pass
- def mk_request(self, user=None, session_key=None, ip='192.168.123.123',
+ def mk_request(self, user=None, ip='192.168.123.123',
user_agent='FakeBrowser 1.0'):
request = HttpRequest()
request.user = user and user or AnonymousUser()
- if session_key:
- request.session = Session()
- request.session.session_key = session_key
request.method = 'GET'
request.META['REMOTE_ADDR'] = ip
request.META['HTTP_USER_AGENT'] = user_agent
return request
+ @attr('bad_multiple')
def test_bad_multiple_counters(self):
"""Force multiple counters, possibly result of race condition, ensure graceful handling"""
- request = self.mk_request()
- user, ip, user_agent, session_key = get_unique(request)
-
+ action_name = "likes"
obj_1 = self.obj_1
obj_1_ct = ContentType.objects.get_for_model(obj_1)
+ request = self.mk_request()
+ user, ip, user_agent, unique_hash = get_unique(obj_1_ct, obj_1.pk,
+ action_name, request)
+
+ # Create an initial counter record directly.
u1 = ActionCounterUnique(content_type=obj_1_ct, object_pk=obj_1.pk,
- name="likes", total=1, ip=ip, user_agent=user_agent, user=user,
- session_key=session_key)
+ name=action_name, total=1, ip=ip, user_agent=user_agent,
+ user=user)
u1.save()
- u2 = ActionCounterUnique(content_type=obj_1_ct, object_pk=obj_1.pk,
- name="likes", total=1, ip=ip, user_agent=user_agent, user=user,
- session_key=session_key)
- u2.save()
-
+ # Adding a duplicate counter should be prevented at the model level.
+ try:
+ u2 = ActionCounterUnique(content_type=obj_1_ct, object_pk=obj_1.pk,
+ name=action_name, total=1, ip=ip, user_agent=user_agent,
+ user=user)
+ u2.save()
+ ok_(False, "This should have triggered an IntegrityError")
+ except:
+ pass
+
+ # Try get_unique_for_request, which should turn up the single unique
+ # record created earlier.
try:
(u, created) = ActionCounterUnique.objects.get_unique_for_request(obj_1,
- 'likes', request)
+ action_name, request)
eq_(False, created)
except MultipleObjectsReturned, e:
ok_(False, "MultipleObjectsReturned should not be raised")
36 apps/actioncounters/utils.py
View
@@ -1,10 +1,13 @@
-from django.conf import settings
import re
import logging
+import hashlib
+
+from django.conf import settings
# this is not intended to be an all-knowing IP address regex
IP_RE = re.compile('\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}')
+
def get_ip(request):
"""
Retrieves the remote IP address from the request data. If the user is
@@ -37,30 +40,31 @@ def get_ip(request):
return ip_address
-def get_unique(request, use_session_key=False):
+def get_unique(content_type, object_pk, name, request=None, ip=None, user_agent=None, user=None):
"""Extract a set of unique identifiers from the request.
This set will be made up of one of the following combinations, depending
on what's available:
- * user, None, None, None
- * None, None, None, session_key
- * None, ip, user_agent, None
+ * user, None, None, unique_MD5_hash
+ * None, ip, user_agent, unique_MD5_hash
"""
- if request.user.is_authenticated():
- user = request.user
- ip = user_agent = session_key = None
- else:
- user = None
- session_key = (
- ( use_session_key and hasattr(request, 'session') ) and
- request.session.session_key or None )
- if session_key:
+ if request:
+ if request.user.is_authenticated():
+ user = request.user
ip = user_agent = None
else:
+ user = None
ip = get_ip(request)
user_agent = request.META.get('HTTP_USER_AGENT', '')[:255]
- return ( user, ip, user_agent, session_key )
-
+ # HACK: Build a hash of the fields that should be unique, let MySQL
+ # chew on that for a unique index. Note that any changes to this algo
+ # will create all new unique hashes that don't match any existing ones.
+ hash_text = "\n".join(unicode(x) for x in (
+ content_type.pk, object_pk, name, ip, user_agent,
+ (user and user.pk or 'None')
+ ))
+ unique_hash = hashlib.md5(hash_text).hexdigest()
+ return (user, ip, user_agent, unique_hash)
95 apps/contentflagging/migrations/0001_initial.py
View
@@ -0,0 +1,95 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding model 'ContentFlag'
+ db.create_table('contentflagging_contentflag', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('flag_status', self.gf('django.db.models.fields.CharField')(default='flagged', max_length=16)),
+ ('flag_type', self.gf('django.db.models.fields.CharField')(max_length=64, db_index=True)),
+ ('explanation', self.gf('django.db.models.fields.TextField')(max_length=255, blank=True)),
+ ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(related_name='content_type_set_for_contentflag', to=orm['contenttypes.ContentType'])),
+ ('object_pk', self.gf('django.db.models.fields.CharField')(max_length=32)),
+ ('ip', self.gf('django.db.models.fields.CharField')(max_length=40, null=True, blank=True)),
+ ('session_key', self.gf('django.db.models.fields.CharField')(max_length=40, null=True, blank=True)),
+ ('user_agent', self.gf('django.db.models.fields.CharField')(max_length=128, null=True, blank=True)),
+ ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)),
+ ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
+ ('modified', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
+ ))
+ db.send_create_signal('contentflagging', ['ContentFlag'])
+
+ # Adding unique constraint on 'ContentFlag', fields ['content_type', 'object_pk', 'ip', 'session_key', 'user_agent', 'user']
+ db.create_unique('contentflagging_contentflag', ['content_type_id', 'object_pk', 'ip', 'session_key', 'user_agent', 'user_id'])
+
+
+ def backwards(self, orm):
+
+ # Removing unique constraint on 'ContentFlag', fields ['content_type', 'object_pk', 'ip', 'session_key', 'user_agent', 'user']
+ db.delete_unique('contentflagging_contentflag', ['content_type_id', 'object_pk', 'ip', 'session_key', 'user_agent', 'user_id'])
+
+ # Deleting model 'ContentFlag'
+ db.delete_table('contentflagging_contentflag')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contentflagging.contentflag': {
+ 'Meta': {'ordering': "('-created',)", 'unique_together': "(('content_type', 'object_pk', 'ip', 'session_key', 'user_agent', 'user'),)", 'object_name': 'ContentFlag'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_contentflag'", 'to': "orm['contenttypes.ContentType']"}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'explanation': ('django.db.models.fields.TextField', [], {'max_length': '255', 'blank': 'True'}),
+ 'flag_status': ('django.db.models.fields.CharField', [], {'default': "'flagged'", 'max_length': '16'}),
+ 'flag_type': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
+ 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'object_pk': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'user_agent': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ }
+ }
+
+ complete_apps = ['contentflagging']
91 apps/contentflagging/migrations/0002_unique_hash_index.py
View
@@ -0,0 +1,91 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ try:
+ # Removing unique constraint on 'ContentFlag', fields ['ip', 'object_pk', 'user_agent', 'content_type', 'session_key', 'user']
+ db.delete_unique('contentflagging_contentflag', ['ip', 'object_pk', 'user_agent', 'content_type_id', 'session_key', 'user_id'])
+ except:
+ # This constraint may have already been removed, so ignore exceptions
+ pass
+
+ # Deleting field 'ContentFlag.session_key'
+ db.delete_column('contentflagging_contentflag', 'session_key')
+
+ # Adding field 'ContentFlag.unique_hash'
+ db.add_column('contentflagging_contentflag', 'unique_hash', self.gf('django.db.models.fields.CharField')(max_length=32, unique=True, null=True, db_index=True), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Adding field 'ContentFlag.session_key'
+ db.add_column('contentflagging_contentflag', 'session_key', self.gf('django.db.models.fields.CharField')(max_length=40, null=True, blank=True), keep_default=False)
+
+ # Deleting field 'ContentFlag.unique_hash'
+ db.delete_column('contentflagging_contentflag', 'unique_hash')
+
+ # Adding unique constraint on 'ContentFlag', fields ['ip', 'object_pk', 'user_agent', 'content_type', 'session_key', 'user']
+ db.create_unique('contentflagging_contentflag', ['ip', 'object_pk', 'user_agent', 'content_type_id', 'session_key', 'user_id'])
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contentflagging.contentflag': {
+ 'Meta': {'ordering': "('-created',)", 'object_name': 'ContentFlag'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_contentflag'", 'to': "orm['contenttypes.ContentType']"}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'explanation': ('django.db.models.fields.TextField', [], {'max_length': '255', 'blank': 'True'}),
+ 'flag_status': ('django.db.models.fields.CharField', [], {'default': "'flagged'", 'max_length': '16'}),
+ 'flag_type': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
+ 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'object_pk': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'unique_hash': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True', 'db_index': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'user_agent': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ }
+ }
+
+ complete_apps = ['contentflagging']
87 apps/contentflagging/migrations/0003_update_unique_hashes.py
View
@@ -0,0 +1,87 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+ "Write your forwards methods here."
+ # Update all remaining flags with a unique hash.
+ from contentflagging.utils import get_unique
+ flags = orm.ContentFlag.objects.all()
+ for flag in flags:
+ try:
+ # Need to duplicate the custom saving code from the model,
+ # since South's frozen version of it doesn't have the logic.
+ user, ip, user_agent, unique_hash = get_unique(
+ flag.content_type, flag.object_pk,
+ ip=flag.ip, user_agent=flag.user_agent, user=flag.user)
+ flag.unique_hash = unique_hash
+ flag.save()
+ except IntegrityError:
+ # If there's already a flag with the unique hash, delete
+ # this one as a duplicate.
+ flag.delete()
+
+
+ def backwards(self, orm):
+ "Write your backwards methods here."
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contentflagging.contentflag': {
+ 'Meta': {'ordering': "('-created',)", 'object_name': 'ContentFlag'},
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'content_type_set_for_contentflag'", 'to': "orm['contenttypes.ContentType']"}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'explanation': ('django.db.models.fields.TextField', [], {'max_length': '255', 'blank': 'True'}),
+ 'flag_status': ('django.db.models.fields.CharField', [], {'default': "'flagged'", 'max_length': '16'}),
+ 'flag_type': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
+ 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'object_pk': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'unique_hash': ('django.db.models.fields.CharField', [], {'max_length': '32', 'unique': 'True', 'null': 'True', 'db_index': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'user_agent': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ }
+ }
+
+ complete_apps = ['contentflagging']
0  apps/contentflagging/migrations/__init__.py
View
No changes.
46 apps/contentflagging/models.py
View
@@ -55,25 +55,17 @@ def flag(self, request, object, flag_type, explanation, recipients=None):
if flag_type not in dict(FLAG_REASONS):
return (None, False)
- user, ip, user_agent, session_key = get_unique(request)
-
content_type = ContentType.objects.get_for_model(object)
+ user, ip, user_agent, unique_hash = get_unique(content_type, object.pk,
+ request=request)
- try:
- cf = ContentFlag.objects.get_or_create(
- content_type=content_type, object_pk=object.pk,
- ip=ip, user_agent=user_agent, user=user, session_key=session_key,
- defaults=dict(flag_type=flag_type, explanation=explanation,))
-
- except MultipleObjectsReturned, e:
- # HACK: There seems to be a race condition in get_or_create. :(
- # Happens very rarely, but when it does, seems like the best thing
- # to do is try to clean up?
- flags = ContentFlag.objects.filter(
- content_type=content_type, object_pk=object.pk,
- ip=ip, user_agent=user_agent, user=user, session_key=session_key).all()
- cf = ( flags[0], False )
- for c in flags[1:]: c.delete()
+ cf = ContentFlag.objects.get_or_create(
+ unique_hash=unique_hash,
+ defaults=dict(content_type=content_type,
+ object_pk=object.pk, ip=ip,
+ user_agent=user_agent, user=user,
+ flag_type=flag_type,
+ explanation=explanation))
if recipients:
subject = _("{object} Flagged")
@@ -94,8 +86,6 @@ class ContentFlag(models.Model):
class Meta:
ordering = ('-created',)
get_latest_by = 'created'
- unique_together = (('content_type', 'object_pk',
- 'ip', 'session_key', 'user_agent', 'user'),)
flag_status = models.CharField(
_('current status of flag review'),
@@ -106,6 +96,7 @@ class Meta:
explanation = models.TextField(
_('please explain what content you feel is inappropriate'),
max_length=255, blank=True)
+
content_type = models.ForeignKey(
ContentType, editable=False,
verbose_name="content type",
@@ -119,15 +110,18 @@ class Meta:
ip = models.CharField(
max_length=40, editable=False,
blank=True, null=True)
- session_key = models.CharField(
- max_length=40, editable=False,
- blank=True, null=True)
user_agent = models.CharField(
max_length=128, editable=False,
blank=True, null=True)
user = models.ForeignKey(
User, editable=False, blank=True, null=True)
+ # HACK: As it turns out, MySQL doesn't consider two rows with NULL values
+ # in a column as duplicates. So, resorting to calculating a unique hash in
+ # code.
+ unique_hash = models.CharField(max_length=32, editable=False,
+ unique=True, db_index=True, null=True)
+
created = models.DateTimeField(
_('date submitted'),
auto_now_add=True, blank=False, editable=False)
@@ -139,6 +133,14 @@ def __unicode__(self):
return 'ContentFlag %(flag_type)s -> "%(title)s"' % dict(
flag_type=self.flag_type, title=str(self.content_object))
+ def save(self, *args, **kwargs):
+ # Ensure unique_hash is updated whenever the object is saved
+ user, ip, user_agent, unique_hash = get_unique(
+ self.content_type, self.object_pk,
+ ip=self.ip, user_agent=self.user_agent, user=self.user)
+ self.unique_hash = unique_hash
+ super(ContentFlag, self).save(*args, **kwargs)
+
def content_view_link(self):
"""HTML link to the absolute URL for the linked content object"""
object = self.content_object
30 apps/contentflagging/tests.py
View
@@ -40,38 +40,44 @@ def tearDown(self):
# logging.debug("SQL %s" % sql)
pass
- def mk_request(self, user=None, session_key=None, ip='192.168.123.123',
+ def mk_request(self, user=None, ip='192.168.123.123',
user_agent='FakeBrowser 1.0'):
request = HttpRequest()
request.user = user and user or AnonymousUser()
- if session_key:
- request.session = Session()
- request.session.session_key = session_key
request.method = 'GET'
request.META['REMOTE_ADDR'] = ip
request.META['HTTP_USER_AGENT'] = user_agent
return request
+ @attr('bad_multiple')
def test_bad_multiple_flags(self):
"""Force multiple flags, possibly result of race condition, ensure graceful handling"""
request = self.mk_request()
- user, ip, user_agent, session_key = get_unique(request)
obj_1 = self.user2
obj_1_ct = ContentType.objects.get_for_model(obj_1)
+ user, ip, user_agent, unique_hash = get_unique(obj_1_ct, obj_1.pk,
+ request=request)
+ # Create an initial record directly.
f1 = ContentFlag(content_type=obj_1_ct, object_pk=obj_1.pk,
flag_type="Broken thing",
- ip=ip, user_agent=user_agent, user=user, session_key=session_key)
+ ip=ip, user_agent=user_agent, user=user)
f1.save()
- f2 = ContentFlag(content_type=obj_1_ct, object_pk=obj_1.pk,
- flag_type="Broken thing",
- ip=ip, user_agent=user_agent, user=user, session_key=session_key)
- f2.save()
-
+ # Adding a duplicate should be prevented at the model level.
+ try:
+ f2 = ContentFlag(content_type=obj_1_ct, object_pk=obj_1.pk,
+ flag_type="Broken thing",
+ ip=ip, user_agent=user_agent, user=user)
+ f2.save()
+ except:
+ pass
+
+ # Try flag, which should turn up the single unique record created
+ # earlier.
try:
- flag, created = ContentFlag.objects.flag(request=request, object=self.user2,
+ flag, created = ContentFlag.objects.flag(request=request, object=obj_1,
flag_type='notworking', explanation="It really not go!")
ok_(flag is not None)
ok_(not created)
36 apps/contentflagging/utils.py
View
@@ -1,6 +1,8 @@
-from django.conf import settings
import re
import logging
+import hashlib
+
+from django.conf import settings
# this is not intended to be an all-knowing IP address regex
IP_RE = re.compile('\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}')
@@ -37,31 +39,31 @@ def get_ip(request):
return ip_address
-def get_unique(request, use_session_key=False):
+def get_unique(content_type, object_pk, request=None, ip=None, user_agent=None, user=None):
"""Extract a set of unique identifiers from the request.
This set will be made up of one of the following combinations, depending
on what's available:
- * user, None, None, None
- * None, None, None, session_key
- * None, ip, user_agent, None
+ * user, None, None, unique_MD5_hash
+ * None, ip, user_agent, unique_MD5_hash
"""
- if request.user.is_authenticated():
- user = request.user
- ip = user_agent = session_key = None
- else:
- user = None
- session_key = (
- ( use_session_key and hasattr(request, 'session') ) and
- request.session.session_key or None )
- if session_key:
+ if request:
+ if request.user.is_authenticated():
+ user = request.user
ip = user_agent = None
else:
+ user = None
ip = get_ip(request)
user_agent = request.META.get('HTTP_USER_AGENT', '')[:255]
- return ( user, ip, user_agent, session_key )
-
-
+ # HACK: Build a hash of the fields that should be unique, let MySQL
+ # chew on that for a unique index. Note that any changes to this algo
+ # will create all new unique hashes that don't match any existing ones.
+ hash_text = "\n".join(unicode(x) for x in (
+ content_type.pk, object_pk, ip, user_agent,
+ (user and user.pk or 'None')
+ ))
+ unique_hash = hashlib.md5(hash_text).hexdigest()
+ return (user, ip, user_agent, unique_hash)
43 apps/demos/__init__.py
View
@@ -12,30 +12,6 @@
except ImportError:
import Image
-
-# TODO: Allow these to be managed via DB / admin page?
-
-# Currently promoted dev derby
-DEMOS_DEVDERBY_CURRENT_CHALLENGE_TAG = getattr(settings, 'DEMOS_DEVDERBY_CURRENT_CHALLENGE_TAG',
- 'challenge:2011:august')
-
-# Dev derby choices displayed on submission form
-DEMOS_DEVDERBY_CHALLENGE_CHOICES = getattr(settings, 'DEMOS_DEVDERBY_CHALLENGE_CHOICES', [
- "challenge:2011:august",
- "challenge:2011:september",
- "challenge:2011:october",
-])
-
-# Dev derby tags for previous challenges
-DEMOS_DEVDERBY_PREVIOUS_CHALLENGE_TAGS = getattr(settings, 'DEMOS_DEVDERBY_PREVIOUS_CHALLENGE_TAGS', [
- "challenge:2011:june",
- "challenge:2011:july",
-])
-
-# Tag used to find most recent winner for dev derby
-DEMOS_DEVDERBY_PREVIOUS_WINNER_TAG = getattr(settings, 'DEMOS_DEVDERBY_PREVIOUS_WINNER_TAG',
- 'system:challenge:firstplace:2011:june')
-
DEMOS_CACHE_NS_KEY = getattr(settings, 'DEMOS_CACHE_NS_KEY', 'demos_listing')
# HACK: For easier L10N, define tag descriptions in code instead of as a DB model
@@ -46,6 +22,8 @@
"title": _("June 2011 Dev Derby Challenge - CSS3 Animations"),
"short_title": _("CSS3 Animations"),
"dateline": _("June 2011"),
+ "short_dateline": _("June"),
+ "tagline": _("Style and experience"),
"summary": _("CSS3 Animations let you change property values over time, to animate the appearance or position of elements, with no or minimal JavaScript, and with greater control than transitions."),
"description": _("CSS3 Animations are a new feature of modern browsers like Firefox, which add even more flexibility and control to the style and experience of the Web. CSS3 Animations let you change property values over time with no or minimal JavaScript, and with greater control than CSS Transitions. Go beyond static properties to animate the appearance and positions of HTML elements. You can achieve these effects without Flash or Silverlight, to make creative dynamic interfaces and engaging animations with CSS3."),
"learn_more": [],
@@ -82,6 +60,9 @@
"summary": _("With Geolocation, you can get the user's physical location (with permission) and use it to enhance the browsing experience or enable advanced location-aware features."),
"description": _("With Geolocation, you can get the user's physical location (with permission) and use it to enhance the browsing experience or enable advanced location-aware features."),
"learn_more": [],
+ "tab_copy": _("""<p>Mobile device users are by now accustomed to "checking in" and getting directions using their devices. The Geolocation API enables web developers to offer features based on the user's location without having to call a native API, much less having to submit an app to a gatekeeper or require the user to install yet another native app.</p>
+<p>With information bout the user's location and movement, you could provide a local guide (say, a muggle's guide to magical London), invent location-based games, or apply global datasets to any location (such as your local weather trends 50 years from now, based on climate change predications).</p>
+<p>Note that the judges may not be able to fully test demos that are specific to remote locations, so a video of such demos is very helpful. Though if we were to get several submissions about Caribbean islands ...</p>"""),
},
{
"tag_name": "challenge:2011:october",
@@ -93,6 +74,20 @@
"summary": _("CSS Media Queries allow Web developers to create responsive Web designs, tailoring the user experience for a range of screen sizes, including desktops, tablets, and mobiles."),
"description": _("CSS Media Queries allow Web developers to create responsive Web designs, tailoring the user experience for a range of screen sizes, including desktops, tablets, and mobiles."),
"learn_more": [],
+ "tab_copy": _("""<p>The range of hardware that can display Web pages is increasing exponentially. Feature phones, smart phones, tablets, e-book readers, game consoles, video players, and high-res widescreen displays now co-exist with the basic laptop and desktop screens of just a few years ago. Older techniques for adapting to device size, such as relying on user agent strings, become impractical with this explosion of diversity. Fortunately, CSS3 media queries enable you to tailor your design based on the physical characteristics of the display device, which is the relevant factor anyway.</p>
+<p>Not just layout, but typography, navigation, and hot spots can all adapt to provide an optimal Web experience for the device of the moment, as the same content shines everywhere.</p>"""),
+ },
+ {
+ "tag_name": "challenge:2011:november",
+ "title": _("November 2011 Dev Derby Challenge - Canvas"),
+ "short_title": _("Canvas"),
+ "dateline": _("November 2011"),
+ "short_dateline": _("November"),
+ "tagline": _("The joy of painting"),
+ "summary": _("Canvas lets you to paint the Web using JavaScript to render 2D shapes, bitmapped images, and advanced graphical effects. Each <canvas> element provides a graphics context with its own state and methods that make it easy to control and draw in."),
+ "description": _("Canvas lets you to paint the Web using JavaScript to render 2D shapes, bitmapped images, and advanced graphical effects. Each <canvas> element provides a graphics context with its own state and methods that make it easy to control and draw in."),
+ "learn_more": [],
+ "tab_copy": _("Canvas lets you to paint the Web using JavaScript to render 2D shapes, bitmapped images, and advanced graphical effects. Each <canvas> element provides a graphics context with its own state and methods that make it easy to control and draw in."),
},
13 apps/demos/forms.py
View
@@ -23,8 +23,7 @@
from django.core.files.base import ContentFile
from django.core.files.uploadedfile import InMemoryUploadedFile
-from . import (scale_image, TAG_DESCRIPTIONS,
- DEMOS_DEVDERBY_CHALLENGE_CHOICES)
+from . import (scale_image, TAG_DESCRIPTIONS)
from .models import Submission
from captcha.fields import ReCaptchaField
@@ -32,7 +31,9 @@
import django.forms.fields
from django.forms.widgets import CheckboxSelectMultiple
-from taggit.utils import parse_tags
+import constance.config
+
+from taggit_extras.utils import parse_tags, split_strip
try:
@@ -106,8 +107,10 @@ class Meta:
widget = CheckboxSelectMultiple,
required = False,
choices = (
- (TAG_DESCRIPTIONS[x]['tag_name'], TAG_DESCRIPTIONS[x]['title'])
- for x in DEMOS_DEVDERBY_CHALLENGE_CHOICES
+ (TAG_DESCRIPTIONS[x]['tag_name'], TAG_DESCRIPTIONS[x]['title'])
+ for x in parse_tags(
+ constance.config.DEMOS_DEVDERBY_CHALLENGE_CHOICE_TAGS,
+ sorted=False)
)
)
11 apps/demos/helpers.py
View
@@ -141,7 +141,7 @@ def submission_listing_cache_key(*args, **kw):
@register_cached_inclusion_tag('demos/elements/submission_listing.html', submission_listing_cache_key)
def submission_listing(request, submission_list, is_paginated, paginator, page_obj, feed_title, feed_url,
- cols_per_row=3, pagination_base_url='', show_sorts=True):
+ cols_per_row=3, pagination_base_url='', show_sorts=True, show_submit=False):
return locals()
@register.inclusion_tag('demos/elements/tech_tags_list.html')
@@ -200,7 +200,7 @@ def license_title(license_name):
@register.function
def tag_title(tag):
if not tag: return ''
- name = ( type(tag) is str ) and tag or tag.name
+ name = (isinstance(tag, basestring)) and tag or tag.name
if name in TAG_DESCRIPTIONS:
return TAG_DESCRIPTIONS[name]['title']
else:
@@ -209,7 +209,7 @@ def tag_title(tag):
@register.function
def tag_description(tag):
if not tag: return ''
- name = ( type(tag) is str ) and tag or tag.name
+ name = (isinstance(tag, basestring)) and tag or tag.name
if name in TAG_DESCRIPTIONS:
return TAG_DESCRIPTIONS[name]['description']
else:
@@ -218,7 +218,8 @@ def tag_description(tag):
@register.function
def tag_learn_more(tag):
if not tag: return ''
- if tag.name in TAG_DESCRIPTIONS and 'learn_more' in TAG_DESCRIPTIONS[tag.name]:
+ if (tag.name in TAG_DESCRIPTIONS and
+ 'learn_more' in TAG_DESCRIPTIONS[tag.name]):
return TAG_DESCRIPTIONS[tag.name]['learn_more']
else:
return []
@@ -228,7 +229,7 @@ def tag_meta(tag, other_name):
"""Get metadata for a tag or tag name."""
# TODO: Replace usage of tag_{title,description,learn_more}?
if not tag: return ''
- name = ( type(tag) is str ) and tag or tag.name
+ name = (isinstance(tag, basestring)) and tag or tag.name
if name in TAG_DESCRIPTIONS and other_name in TAG_DESCRIPTIONS[name]:
return TAG_DESCRIPTIONS[name][other_name]
else:
2  apps/demos/templates/demos/detail.html
View
@@ -266,6 +266,7 @@ <h3 class="mod-title">{{_('Get the Source Code')}}</h3>
</p>
</div>
+ {% if more_by | length > 1 %}
<div class="module" id="moreby">
<h3 class="mod-title">More by {{ submission_creator(submission) }}</h3>
<ul class="gallery">
@@ -276,6 +277,7 @@ <h3 class="mod-title">More by {{ submission_creator(submission) }}</h3>
{% endfor %}
</ul>
</div>
+ {% endif %}
<div id="demo-report">
<h3>Report a Problem</h3>
113 apps/demos/templates/demos/devderby_landing.html
View
@@ -35,7 +35,7 @@
<header id="derby-head">
<p class="presents"><a href="{{ url('demos') }}">{{_("Mozilla Demo Studio")}}</a> {{_("presents")}}</p>
<h1>{{_("Dev Derby")}}</h1>
- <h2>{{_("Show us what you can do with the History API")}}</h2>
+ <h2>{% trans title=tag_meta(current_challenge_tag_name, "short_title") %}Show us what you can do with {{ title }}{% endtrans %}</h2>
<p class="info">{{_("Join the Dev Derby now and submit your demo to win an Android phone or other prizes.")}}</p>
<p class="submit">{% trans %}<a href="{{ submit_url }}"><b>Submit</b> Your Demo</a>{% endtrans %}</p>
</header>
@@ -47,11 +47,11 @@
</header>
<ol>
<li class="first">
- <h2 class="title">{{tag_meta(challenge_choices[0], "short_title")}}</h2>
- <h3 class="date">{{tag_meta(challenge_choices[0], "short_dateline")}}</h3>
- <h4 class="tagline">{{tag_meta(challenge_choices[0], "tagline")}}</h4>
+ <h2 class="title">{{tag_meta(current_challenge_tag_name, "short_title")}}</h2>
+ <h3 class="date">{{tag_meta(current_challenge_tag_name, "short_dateline")}}</h3>
+ <h4 class="tagline">{{tag_meta(current_challenge_tag_name, "tagline")}}</h4>
<h5 class="current">{% trans %}<span><b>Current</b> Derby</span>{% endtrans %}</h5>
- <p class="desc">{{tag_meta(challenge_choices[0], "summary")}}</p>
+ <p class="desc">{{tag_meta(current_challenge_tag_name, "summary")}}</p>
</li>
<li class="second">
<h2 class="title">{{tag_meta(challenge_choices[1], "short_title")}}</h2>
@@ -79,20 +79,13 @@ <h4 class="tagline">{{tag_meta(challenge_choices[2], "tagline")}}</h4>
<section id="tab-challenge" class="block">
<header>
- <h1 class="title">{{_("History API")}}</h1>
- <h2 class="date">{{_("August")}}</h2>
+ <h1 class="title">{{tag_meta(current_challenge_tag_name, "short_title")}}</h1>
+ <h2 class="date">{{tag_meta(current_challenge_tag_name, "short_dateline")}}</h2>
</header>
- <p class="tagline">{{_("A browser never forgets.")}}</p>
- {% trans %}
- <p>Creating a great user experience for web apps is a challenge. When you use JavaScript
- to avoid page loads on most interactions (good), you break the user's expectation that
- the Back button and bookmarks will return to a previous state (not so good). With the
- HTML5 History API, you can manipulate the contents of the browser history, so that you
- can both meet users' expectations about browser behavior, and provide a responsive app.</p>
-
- <p>What kind of "time-travel" or "alternative history" can you dream up with the History API?</p>
-
- <p class="demo-submit"><a href="{{ submit_url }}" class="button">Submit your demo</a> for the August Dev Derby today!</p>
+ <p class="tagline">{{tag_meta(current_challenge_tag_name, "tagline")}}</p>
+ {{tag_meta(current_challenge_tag_name, "tab_copy")|safe}}
+ {% trans month=tag_meta(current_challenge_tag_name, "short_dateline") %}
+ <p class="demo-submit"><a href="{{ submit_url }}" class="button">Submit your demo</a> for the {{month}} Dev Derby today!</p>
{% endtrans %}
</section>
@@ -148,6 +141,57 @@ <h2 class="date">{{_("August")}}</h2>
<h2>Expert Judges</h2>
<ul class="judges">
<li class="vcard">
+ <h3><a href="http://isofarro.com" class="fn url">Mike Davies <img src="{{MEDIA_URL}}img/devderby/judges/mikedavies.jpg" alt="" class="photo" width="100" height="100"></a></h3>
+ <h4 class="title">Web Developer, Retro-gamer</h4>
+ <p class="twitter"><a href="http://twitter.com/isofarro" class="url nickname">@isofarro</a></p>
+ <p>Mike Davies is a Senior Web Developer at <a href="http://www.lovefilm.com/">LOVEFiLM</a>
+ based in West London. He has over 10 years of commercial web development experience, including
+ a stint at Yahoo! Europe. One of his notable career achievements was his lead-developer role
+ for Legal &amp; General's 2005 <a href="http://www.w3.org/WAI/bcase/legal-and-general-case-study">commercially successful accessibility redesign</a>.
+ Mike is passionate about high-quality web development, and Lords of Midnight on the ZX Spectrum.
+ He also worries about online privacy.</p>
+ </li>
+ <li class="vcard">
+ <h3><a href="http://wonko.com" class="fn url">Ryan Grove <img src="{{MEDIA_URL}}img/devderby/judges/ryangrove.jpg" alt="" class="photo" width="100" height="100"></a></h3>
+ <h4 class="title">YUI Engineer at Yahoo!</h4>
+ <p class="twitter"><a href="http://twitter.com/yaypie" class="url nickname">@yaypie</a></p>
+ <p>Ryan Grove works on YUI at Yahoo!. His love of JavaScript is surpassed only by his
+ love of pie. He lives the dream of the 90s in Portland, Oregon, where the weather usually
+ provides a perfect excuse to stay inside eating pie and writing code.</p>
+ </li>
+ <li class="vcard">
+ <h3><a href="http://encoding.com" class="fn url">Jeff Malkin <img src="{{MEDIA_URL}}img/devderby/judges/jeffmalkin.jpg" alt="" class="photo" width="100" height="100"></a></h3>
+ <h4 class="title">President, Encoding.com</h4>
+ <p class="twitter"><a href="http://twitter.com/jeffmalkin" class="url nickname">@jeffmalkin</a></p>
+ <p>Jeff Malkin is a fearless entrepreneur with a proven track record in growing technology
+ startups in the Internet and mobile sectors. Jeff has guided Encoding.com, a Gartner “Cool
+ Vendor 2011,” to its position as the world's largest video encoding service for web and
+ mobile video, with over 2,000 clients and more than 10 million encodes. Jeff was recently
+ named a Streaming Media All Star for 2011.</p>
+ </li>
+ <li class="vcard">
+ <h3><a href="http://nimbupani.com" class="fn url">Divya Manian <img src="{{MEDIA_URL}}img/devderby/judges/divyamanian.jpg" alt="" class="photo" width="100" height="100"></a></h3>
+ <h4 class="title">Web Opener at Opera, Open Web Vigilante</h4>
+ <p class="twitter"><a href="http://twitter.com/divya" class="url nickname">@divya</a></p>
+ <p>Divya Manian is a Web Opener for Opera Software in Seattle. She made the jump from
+ developing device drivers for Motorola phones to designing websites and has not looked
+ back since. She takes her duties as an Open Web vigilante seriously which has resulted
+ in collaborative projects such as <a href="http://html5readiness.com">HTML5 Readiness</a>
+ and <a href="http://html5boilerplate.com">HTML5 Boilerplate</a>.</p>
+ </li>
+ <li class="vcard">
+ <h3><a href="http://ethanmarcotte.com" class="fn url">Ethan Marcotte <img src="{{MEDIA_URL}}img/devderby/judges/ethanmarcotte.jpg" alt="" class="photo" width="100" height="100" title="Photo by Anton Peck"></a></h3>
+ <h4 class="title">Web designer, author</h4>
+ <p class="twitter"><a href="http://twitter.com/beep" class="url nickname">@beep</a></p>
+ <p><a href="http://ethanmarcotte.com/">Ethan Marcotte</a> is a web designer &amp; developer
+ who cares deeply about beautiful design, elegant code, and the intersection of the two. Over
+ the years, Ethan has enjoyed working with such clients as the Sundance Film Festival, Stanford
+ University, <cite>New&nbsp;York Magazine</cite> and The Today Show. He swears profusely
+ <a href="http://twitter.com/beep">on Twitter</a>, and would like to be an
+ <a href="http://unstoppablerobotninja.com/" class="url">unstoppable robot ninja</a> when he grows up. His
+ most recent book is <cite><a href="http://www.abookapart.com/products/responsive-web-design">Responsive Web Design</a></cite>.</p>
+ </li>
+ <li class="vcard">
<h3><a href="http://leaverou.me" class="fn url">Lea Verou <img src="{{MEDIA_URL}}img/devderby/judges/leaverou.jpg" alt="" class="photo" width="100" height="100"></a></h3>
<h4 class="title">Web developer, Co-founder of Fresset Ltd.</h4>
<p class="twitter"><a href="http://twitter.com/leaverou" class="url nickname">@leaverou</a></p>
@@ -161,26 +205,23 @@ <h4 class="title">Web developer, Co-founder of Fresset Ltd.</h4>
and Business.</p>
</li>
<li class="vcard">
- <h3><a href="http://ethanmarcotte.com" class="fn url">Ethan Marcotte <img src="{{MEDIA_URL}}img/devderby/judges/ethanmarcotte.jpg" alt="" class="photo" width="100" height="100" title="Photo by Anton Peck"></a></h3>
- <h4 class="title">Web designer, author</h4>
- <p class="twitter"><a href="http://twitter.com/beep" class="url nickname">@beep</a></p>
- <p>Ethan Marcotte is a web designer &amp; developer who cares deeply about beautiful design,
- elegant code, and the intersection of the two. Over the years, Ethan has enjoyed working
- with such clients as the Sundance Film Festival, Stanford University, <cite>New&nbsp;York Magazine</cite>
- and The Today Show. He swears profusely <a href="http://twitter.com/beep">on Twitter</a>,
- and would like to be an <a href="http://unstoppablerobotninja.com/" class="url">unstoppable robot ninja</a>
- when he grows up. His most recent book is <cite><a href="http://www.abookapart.com/products/responsive-web-design">Responsive Web Design</a></cite>.</p>
+ <h3><a href="https://github.com/defunkt" class="fn url">Chris Wanstrath <img src="{{MEDIA_URL}}img/devderby/judges/chriswanstrath.jpg" alt="" class="photo" width="100" height="100"></a></h3>
+ <h4 class="title">Developer, GitHub co-founder</h4>
+ <p class="twitter"><a href="http://twitter.com/defunkt" class="url nickname">@defunkt</a></p>
+ <p>Chris Wanstrath lives in San Francisco and co-founded <a href="http://github.com">GitHub</a>.
+ He likes HTML, guitars, and coffee.</p>
</li>
<li class="vcard">
- <h3><a href="http://nimbupani.com" class="fn url">Divya Manian <img src="{{MEDIA_URL}}img/devderby/judges/divyamanian.jpg" alt="" class="photo" width="100" height="100"></a></h3>
- <h4 class="title">Web Opener at Opera, Open Web Vigilante</h4>
- <p class="twitter"><a href="http://twitter.com/divya" class="url nickname">@divya</a></p>
- <p>Divya Manian is a Web Opener for Opera Software in Seattle. She made the jump from
- developing device drivers for Motorola phones to designing websites and has not looked
- back since. She takes her duties as an Open Web vigilante seriously which has resulted
- in collaborative projects such as <a href="http://html5readiness.com">HTML5 Readiness</a>
- and <a href="http://html5boilerplate.com">HTML5 Boilerplate</a>.</p>
- </li>
+ <h3><a href="http://benward.me/" class="fn url">Ben Ward <img src="{{MEDIA_URL}}img/devderby/judges/benward.jpg" alt="" class="photo" width="100" height="100"></a></h3>
+ <h4 class="title">Front-end developer at Twitter</h4>
+ <p class="twitter"><a href="http://twitter.com/benward" class="url nickname">@benward</a></p>
+ <p>Ben Ward is a front-end developer on Twitter's Platform team. He works on products
+ that put Twitter into other contexts all around the web and in applications. Ben cares
+ a great deal about building on robust content with progressive enhancement and designing
+ for failure. He's written at length about and in awe of the linkable, resource-based
+ architecture of the web. He's also a semantics nerd, administrating and editing
+ specifications at <a href="http://microformats.org">microformats.org</a>.</p>
+ </li>
</ul>
{% endtrans %}
</section>
2  apps/demos/templates/demos/elements/profile_link.html
View
@@ -1,2 +1,2 @@
{% set display_name = user.first_name | default(user.username, true) %}
-<a href="{{ url('demos_profile_detail', username=user.username) }}" class="url fn" title="{{_('See {display_name}\'s profile') | f(display_name=display_name)}}">{% if show_gravatar %}<img src="{{ user.get_profile().gravatar }}" class="photo avatar" width="{{ gravatar_size }}" height="{{ gravatar_size }}" border="0" /> {% endif %}<span>{{ display_name }}</span></a>
+<a href="{{ url('devmo.views.profile_view', username=user.username) }}" class="url fn" title="{{_('See {display_name}\'s profile') | f(display_name=display_name)}}">{% if show_gravatar %}<img src="{{ user.get_profile().gravatar }}" class="photo avatar" width="{{ gravatar_size }}" height="{{ gravatar_size }}" border="0" /> {% endif %}<span>{{ display_name }}</span></a>
2  apps/demos/templates/demos/elements/submission_creator.html
View
@@ -1,2 +1,2 @@
{% set display_name = submission.creator.first_name | default(submission.creator.username, true) %}
-<a href="{{ url('demos_profile_detail', username=submission.creator.username) }}" class="url fn" title="{{_('See {display_name}\'s profile') | f(display_name=display_name)}}">{{ display_name }}</a>
+<a href="{{ url('devmo.views.profile_view', username=submission.creator.username) }}" class="url fn" title="{{_('See {display_name}\'s profile') | f(display_name=display_name)}}">{{ display_name }}</a>
11 apps/demos/templates/demos/elements/submission_listing.html
View
@@ -3,7 +3,7 @@
{% if show_sorts %}
<div class="gallery-head">
- <h2 class="count">{{ ngettext('{count} Demo', '{count} Demos', count) | f(count=paginator.count) }}</h2>
+ <h2 class="count">{{ ngettext('{count} Demo', '{count} Demos', paginator.count) | f(count=paginator.count) }}{% if show_submit %} <a class="button positive" href="{{ url('demos.views.submit') }}">Submit a Demo</a>{% endif %}</h2>
<ul class="sort">
{% set sort_orders = (
( 'upandcoming', _('Sort demos by most recent likes and views'), _('You are viewing demos by most recent likes and views'), _('Up and Coming') ),
@@ -50,15 +50,18 @@ <h2 class="count">{{ ngettext('{count} Demo', '{count} Demos', count) | f(count=
{% endif %}
{% if is_paginated %}
<ul class="paging">
- <!-- No first or prev when we're on the first page, no next or last when we're on the last page -->
- <li class="first"><a href="{{ pagination_base_url }}?page=1&sort={{current_sort}}" title="{{_('Go to the first page')}}">First</a></li>
+ {% if page_obj.number != 1 %}
+ <li class="first"><a href="{{ pagination_base_url }}?page=1&sort={{current_sort}}" title="{{_('Go to the first page')}}">First</a></li>
+ {% endif %}
{% if page_obj.has_previous() %}
<li class="prev"><a href="{{ pagination_base_url }}?page={{ page_obj.previous_page_number() }}&sort={{current_sort}}" title="{{_('Go to the previous page')}}">{{_('Previous')}}</a></li>
{% endif %}
{% if page_obj.has_next() %}
<li class="next"><a href="{{ pagination_base_url }}?page={{ page_obj.next_page_number() }}&sort={{current_sort}}" title="{{_('Go to the next page')}}">{{_('Next')}}</a></li>
{% endif %}
- <li class="last"><a href="{{ pagination_base_url }}?page={{ paginator.num_pages }}&sort={{current_sort}}" title="{{_('Go to the last page')}}">{{_('Last')}}</a></li>
+ {% if page_obj.number != paginator.num_pages %}
+ <li class="last"><a href="{{ pagination_base_url }}?page={{ paginator.num_pages }}&sort={{current_sort}}" title="{{_('Go to the last page')}}">{{_('Last')}}</a></li>
+ {% endif %}
</ul>
{% endif %}
{% if feed_url %}
12 apps/demos/tests/test_views.py
View
@@ -8,7 +8,7 @@
from django import http, test
from django.contrib.auth.models import User
-from sumo.urlresolvers import reverse
+from funfactory.urlresolvers import reverse
from sumo.tests import LocalizingClient
from mock import patch
@@ -124,7 +124,7 @@ def test_submit_post_valid(self):
@logged_in
def test_edit_invalid(self):
s = save_valid_submission()
- edit_url = reverse('demos_edit', args=[s.slug], locale='en-US')
+ edit_url = reverse('demos_edit', args=[s.slug])
r = self.client.post(edit_url, data=dict())
d = pq(r.content)
assert d('form#demo-submit')
@@ -135,7 +135,7 @@ def test_edit_invalid(self):
@logged_in
def test_edit_valid(self):
s = save_valid_submission()
- edit_url = reverse('demos_edit', args=[s.slug], locale='en-US')
+ edit_url = reverse('demos_edit', args=[s.slug])
r = self.client.post(edit_url, data=dict(
title=s.title,
summary='This is a test demo submission',
@@ -195,7 +195,7 @@ def test_creator_can_edit(self):
d = pq(r.content)
edit_link = d('ul#demo-manage a.edit')
assert edit_link
- edit_url = reverse('demos_edit', args=[s.slug], locale='en-US')
+ edit_url = reverse('demos_edit', args=[s.slug])
eq_(edit_url, edit_link.attr("href"))
r = self.client.get(edit_url)
@@ -207,7 +207,7 @@ def test_creator_can_edit(self):
def test_hidden_field(self):
s = save_valid_submission('hello world')
- edit_url = reverse('demos_edit', args=[s.slug], locale='en-US')
+ edit_url = reverse('demos_edit', args=[s.slug])
r = self.client.get(edit_url)
assert pq(r.content)('input[name="hidden"][type="checkbox"]')
@@ -215,6 +215,6 @@ def test_hidden_field(self):
def test_derby_field(self):
s = save_valid_submission('hello world')
- edit_url = reverse('demos_edit', args=[s.slug], locale='en-US')
+ edit_url = reverse('demos_edit', args=[s.slug])
r = self.client.get(edit_url)
assert pq(r.content)('fieldset#devderby-submit')
62 apps/demos/views.py
View
@@ -27,17 +27,16 @@
from django.contrib.auth.models import User
from devmo.models import UserProfile
+import constance.config
+
from taggit.models import Tag
+from taggit_extras.utils import parse_tags, split_strip
+
from demos.models import Submission
from demos.forms import SubmissionNewForm, SubmissionEditForm
-# TODO: Make these configurable in the DB via an admin page
-from . import ( DEMOS_CACHE_NS_KEY,
- DEMOS_DEVDERBY_CURRENT_CHALLENGE_TAG,
- DEMOS_DEVDERBY_PREVIOUS_WINNER_TAG,
- DEMOS_DEVDERBY_PREVIOUS_CHALLENGE_TAGS,
- DEMOS_DEVDERBY_CHALLENGE_CHOICES )
+from . import DEMOS_CACHE_NS_KEY
from contentflagging.models import ContentFlag, FLAG_NOTIFICATIONS
from contentflagging.forms import ContentFlagForm
@@ -162,23 +161,8 @@ def search(request):
template_name='demos/listing_search.html')
def profile_detail(request, username):
- user = get_object_or_404(User, username=username)
- profile = user.get_profile()
-
- sort_order = request.GET.get('sort', 'created')
- show_hidden = user == request.user
- queryset = Submission.objects.all_sorted(sort_order).filter(creator=user)
- if not show_hidden:
- queryset = queryset.exclude(hidden=True)
- return object_list(request, queryset,
- extra_context=dict(
- profile_user=user,
- profile=profile
- ),
- paginate_by=25, allow_empty=True,
- template_loader=template_loader,
- template_object_name='submission',
- template_name='demos/profile_detail.html')
+ return HttpResponseRedirect(reverse(
+ 'devmo.views.profile_view', args=(username,)))
def like(request, slug):
submission = get_object_or_404(Submission, slug=slug)
@@ -249,7 +233,10 @@ def submit(request):
return jingo.render(request, 'demos/submit_noauth.html', {})
if request.method != "POST":
- form = SubmissionNewForm(request_user=request.user)
+ initial = {}
+ if 'tags' in request.GET:
+ initial['challenge_tags'] = parse_tags(request.GET['tags'])
+ form = SubmissionNewForm(initial=initial, request_user=request.user)
else:
form = SubmissionNewForm(request.POST, request.FILES, request_user=request.user)
if form.is_valid():
@@ -368,18 +355,25 @@ def devderby_landing(request):
sort_order = request.GET.get('sort', 'created')
- # TODO: Make these configurable in the DB via an admin page
- current_challenge_tag_name = DEMOS_DEVDERBY_CURRENT_CHALLENGE_TAG
- previous_winner_tag_name = DEMOS_DEVDERBY_PREVIOUS_WINNER_TAG
- previous_challenge_tag_names = DEMOS_DEVDERBY_PREVIOUS_CHALLENGE_TAGS
-
- submissions_qs = ( Submission.objects.all_sorted(sort_order)
+ # Grab current arrangement of challenges from Constance settings
+ current_challenge_tag_name = str(
+ constance.config.DEMOS_DEVDERBY_CURRENT_CHALLENGE_TAG).strip()
+ previous_winner_tag_name = str(
+ constance.config.DEMOS_DEVDERBY_PREVIOUS_WINNER_TAG).strip()
+ previous_challenge_tag_names = parse_tags(
+ constance.config.DEMOS_DEVDERBY_PREVIOUS_CHALLENGE_TAGS,
+ sorted=False)
+ challenge_choices = parse_tags(
+ constance.config.DEMOS_DEVDERBY_CHALLENGE_CHOICE_TAGS,
+ sorted=False)
+
+ submissions_qs = (Submission.objects.all_sorted(sort_order)
.filter(taggit_tags__name__in=[current_challenge_tag_name])
- .exclude(hidden=True) )
+ .exclude(hidden=True))
- previous_winner_qs = ( Submission.objects.all()
+ previous_winner_qs = (Submission.objects.all()
.filter(taggit_tags__name__in=[previous_winner_tag_name])
- .exclude(hidden=True) )
+ .exclude(hidden=True))
# TODO: Use an object_list here, in case we need pagination?
return jingo.render(request, 'demos/devderby_landing.html', dict(
@@ -388,7 +382,7 @@ def devderby_landing(request):
previous_challenge_tag_names = previous_challenge_tag_names,
submissions_qs = submissions_qs,
previous_winner_qs = previous_winner_qs,
- challenge_choices = DEMOS_DEVDERBY_CHALLENGE_CHOICES,
+ challenge_choices = challenge_choices,
))
def devderby_rules(request):
2  apps/devmo/__init__.py
View
@@ -54,4 +54,6 @@ class SECTION_WEB:
"tech writing",
"user experience",
"design",
+ "technical review",
+ "editorial review",
]
6 apps/devmo/fix