From 9f4902f9c921b82f924fe0af106fa5480ca10ca9 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 7 Apr 2011 14:39:14 -0500 Subject: Ensure feed GUIDs are unchanging and unique Implement 'tag:' style URIs for the GUID field on our RSS feeds. This ensures new package updates show up as new, and we aren't jumping back and forth between generated GUIDs having 'http://' and 'https://' prefixes. Much of the work here is to attempt to keep old news GUIDs constant so we don't once again make everything show up as new in newsreaders. Signed-off-by: Dan McGee --- news/migrations/0007_add_guid.py | 65 ++++++++++++++++++++++++++ news/migrations/0008_set_prior_guids.py | 83 +++++++++++++++++++++++++++++++++ news/models.py | 24 ++++++++-- 3 files changed, 168 insertions(+), 4 deletions(-) create mode 100644 news/migrations/0007_add_guid.py create mode 100644 news/migrations/0008_set_prior_guids.py (limited to 'news') diff --git a/news/migrations/0007_add_guid.py b/news/migrations/0007_add_guid.py new file mode 100644 index 00000000..5fa8193e --- /dev/null +++ b/news/migrations/0007_add_guid.py @@ -0,0 +1,65 @@ +# 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): + db.add_column('news', 'guid', self.gf('django.db.models.fields.CharField')(default='', max_length=255), keep_default=False) + + def backwards(self, orm): + db.delete_column('news', 'guid') + + 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'}) + }, + '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'}) + }, + 'news.news': { + 'Meta': {'ordering': "['-postdate']", 'object_name': 'News', 'db_table': "'news'"}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'news_author'", 'to': "orm['auth.User']"}), + 'content': ('django.db.models.fields.TextField', [], {}), + 'guid': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'postdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['news'] diff --git a/news/migrations/0008_set_prior_guids.py b/news/migrations/0008_set_prior_guids.py new file mode 100644 index 00000000..704b11c9 --- /dev/null +++ b/news/migrations/0008_set_prior_guids.py @@ -0,0 +1,83 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.conf import settings +from django.db import models + +class Migration(DataMigration): + '''The point of this migration is to not mark every news item as 'new' in + people's feed readers, and store the GUID perminantly with the news item. + All previously published news items will get their former auto-assigned + GUID; new ones will get a generated tag: URI and this won't apply to + them.''' + + def forwards(self, orm): + all_news = orm.News.objects.all().defer('content') + site = orm['sites.site'].objects.get(pk=settings.SITE_ID).domain + for news in all_news: + new_guid = 'http://%s/news/%s/' % (site, news.slug) + # looks totally silly, but prevents full updates of all fields, + # including content and last_modified which we want to leave alone + orm.News.objects.filter(pk=news.pk).update(guid=new_guid) + + def backwards(self, orm): + pass + + 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'}) + }, + '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'}) + }, + 'news.news': { + 'Meta': {'ordering': "['-postdate']", 'object_name': 'News', 'db_table': "'news'"}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'news_author'", 'to': "orm['auth.User']"}), + 'content': ('django.db.models.fields.TextField', [], {}), + 'guid': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}), + 'postdate': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'sites.site': { + 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"}, + 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + } + } + + complete_apps = ['sites', 'news'] diff --git a/news/models.py b/news/models.py index c2d644b7..c4fb136a 100644 --- a/news/models.py +++ b/news/models.py @@ -1,13 +1,16 @@ +from datetime import datetime + from django.db import models from django.contrib.auth.models import User +from django.contrib.sites.models import Site class News(models.Model): slug = models.SlugField(max_length=255, unique=True) author = models.ForeignKey(User, related_name='news_author') - postdate = models.DateTimeField("post date", auto_now_add=True, db_index=True) - last_modified = models.DateTimeField(editable=False, - auto_now=True, db_index=True) + postdate = models.DateTimeField("post date", db_index=True) + last_modified = models.DateTimeField(editable=False, db_index=True) title = models.CharField(max_length=255) + guid = models.CharField(max_length=255, editable=False) content = models.TextField() def get_absolute_url(self): @@ -22,10 +25,23 @@ class Meta: get_latest_by = 'postdate' ordering = ['-postdate'] +def set_news_fields(sender, **kwargs): + news = kwargs['instance'] + now = datetime.now() + news.last_modified = now + if not news.postdate: + news.postdate = now + # http://diveintomark.org/archives/2004/05/28/howto-atom-id + news.guid = 'tag:%s,%s:%s' % (Site.objects.get_current(), + now.strftime('%Y-%m-%d'), news.get_absolute_url()) + # connect signals needed to keep cache in line with reality from main.utils import refresh_news_latest -from django.db.models.signals import post_save +from django.db.models.signals import pre_save, post_save + post_save.connect(refresh_news_latest, sender=News, dispatch_uid="news.models") +pre_save.connect(set_news_fields, sender=News, + dispatch_uid="news.models") # vim: set ts=4 sw=4 et: -- cgit v1.2.3-54-g00ecf From 01db07bad844e17e084f650b6732647f77a91c5c Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 7 Apr 2011 15:39:53 -0500 Subject: Use UTC datetime objects everywhere Rather than the twisted mix of local times and UTC times we currently have. Signed-off-by: Dan McGee --- devel/management/commands/reporead.py | 6 +++--- feeds.py | 22 ++++------------------ main/models.py | 13 +++++++++++-- news/models.py | 2 +- packages/views.py | 2 +- 5 files changed, 20 insertions(+), 25 deletions(-) (limited to 'news') diff --git a/devel/management/commands/reporead.py b/devel/management/commands/reporead.py index e26bb800..a8875c7e 100644 --- a/devel/management/commands/reporead.py +++ b/devel/management/commands/reporead.py @@ -315,7 +315,7 @@ def populate_files(dbpkg, repopkg, force=False): directory=dirname + '/', filename=filename) pkgfile.save(force_insert=True) - dbpkg.files_last_update = datetime.now() + dbpkg.files_last_update = datetime.utcnow() dbpkg.save() @transaction.commit_on_success @@ -374,7 +374,7 @@ def db_update(archname, reponame, pkgs, options): for p in [x for x in pkgs if x.name in in_sync_not_db]: logger.info("Adding package %s", p.name) pkg = Package(pkgname = p.name, arch = architecture, repo = repository) - populate_pkg(pkg, p, timestamp=datetime.now()) + populate_pkg(pkg, p, timestamp=datetime.utcnow()) # packages in database and not in syncdb (remove from database) in_db_not_sync = dbset - syncset @@ -398,7 +398,7 @@ def db_update(archname, reponame, pkgs, options): if not force: continue else: - timestamp = datetime.now() + timestamp = datetime.utcnow() if filesonly: logger.debug("Checking files for package %s in database", p.name) populate_files(dbp, p, force=force) diff --git a/feeds.py b/feeds.py index 7a2f2e40..0be12531 100644 --- a/feeds.py +++ b/feeds.py @@ -1,5 +1,4 @@ -import datetime -from decimal import Decimal, ROUND_HALF_DOWN +import pytz from django.contrib.sites.models import Site from django.contrib.syndication.views import Feed @@ -10,7 +9,7 @@ from django.views.decorators.http import condition from main.models import Arch, Repo, Package -from main.utils import CACHE_TIMEOUT, INVALIDATE_TIMEOUT +from main.utils import CACHE_TIMEOUT from main.utils import CACHE_PACKAGE_KEY, CACHE_NEWS_KEY from news.models import News @@ -32,17 +31,6 @@ def write_items(self, handler): super(GuidNotPermalinkFeed, self).write_items(handler) -def utc_offset(): - '''Calculate the UTC offset from local time. Useful for converting values - stored in local time to things like cache last modifed headers.''' - timediff = datetime.datetime.utcnow() - datetime.datetime.now() - secs = timediff.days * 86400 + timediff.seconds - # round to nearest minute - mins = Decimal(secs) / Decimal(60) - mins = mins.quantize(Decimal('0'), rounding=ROUND_HALF_DOWN) - return datetime.timedelta(minutes=int(mins)) - - def retrieve_package_latest(): # we could break this down based on the request url, but it would probably # cost us more in query time to do so. @@ -52,7 +40,6 @@ def retrieve_package_latest(): try: latest = Package.objects.values('last_update').latest( 'last_update')['last_update'] - latest = latest + utc_offset() # Using add means "don't overwrite anything in there". What could be in # there is an explicit None value that our refresh signal set, which # means we want to avoid race condition possibilities for a bit. @@ -132,7 +119,7 @@ def item_guid(self, item): date.strftime('%Y%m%d%H%M')) def item_pubdate(self, item): - return item.last_update + return item.last_update.replace(tzinfo=pytz.utc) def item_categories(self, item): return (item.repo.name, item.arch.name) @@ -145,7 +132,6 @@ def retrieve_news_latest(): try: latest = News.objects.values('last_modified').latest( 'last_modified')['last_modified'] - latest = latest + utc_offset() # same thoughts apply as in retrieve_package_latest cache.add(CACHE_NEWS_KEY, latest, CACHE_TIMEOUT) return latest @@ -184,7 +170,7 @@ def item_guid(self, item): return item.guid def item_pubdate(self, item): - return item.postdate + return item.postdate.replace(tzinfo=pytz.utc) def item_author_name(self, item): return item.author.get_full_name() diff --git a/main/models.py b/main/models.py index 4370fa24..8d34731f 100644 --- a/main/models.py +++ b/main/models.py @@ -5,6 +5,7 @@ from main.utils import cache_function, make_choice from packages.models import PackageRelation +from datetime import datetime from itertools import groupby import pytz @@ -351,7 +352,7 @@ class Todolist(models.Model): creator = models.ForeignKey(User) name = models.CharField(max_length=255) description = models.TextField() - date_added = models.DateTimeField(auto_now_add=True, db_index=True) + date_added = models.DateTimeField(db_index=True) objects = TodolistManager() def __unicode__(self): @@ -383,10 +384,18 @@ class Meta: db_table = 'todolist_pkgs' unique_together = (('list','pkg'),) +def set_todolist_fields(sender, **kwargs): + todolist = kwargs['instance'] + if not todolist.date_added: + todolist.date_added = datetime.utcnow() + # connect signals needed to keep cache in line with reality from main.utils import refresh_package_latest -from django.db.models.signals import post_save +from django.db.models.signals import pre_save, post_save + post_save.connect(refresh_package_latest, sender=Package, dispatch_uid="main.models") +pre_save.connect(set_todolist_fields, sender=Todolist, + dispatch_uid="main.models") # vim: set ts=4 sw=4 et: diff --git a/news/models.py b/news/models.py index c4fb136a..17d51de9 100644 --- a/news/models.py +++ b/news/models.py @@ -27,7 +27,7 @@ class Meta: def set_news_fields(sender, **kwargs): news = kwargs['instance'] - now = datetime.now() + now = datetime.utcnow() news.last_modified = now if not news.postdate: news.postdate = now diff --git a/packages/views.py b/packages/views.py index 2f614e34..1587563b 100644 --- a/packages/views.py +++ b/packages/views.py @@ -371,7 +371,7 @@ def flag(request, name, repo, arch): # find all packages from (hopefully) the same PKGBUILD pkgs = Package.objects.filter( pkgbase=pkg.pkgbase, repo__testing=pkg.repo.testing) - pkgs.update(flag_date=datetime.now()) + pkgs.update(flag_date=datetime.utcnow()) maints = pkg.maintainers if not maints: -- cgit v1.2.3-54-g00ecf From d7665959652171b93db5e084c6738e8e1773e7f0 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 7 Apr 2011 16:14:48 -0500 Subject: Add some migrations to convert database to UTC time This follows the earlier commit where we make sure any value going to or being pulled from the database is UTC. Signed-off-by: Dan McGee --- main/migrations/0047_utc_datetimes.py | 180 ++++++++++++++++++++++++++++++++++ news/migrations/0009_utc_datetimes.py | 85 ++++++++++++++++ 2 files changed, 265 insertions(+) create mode 100644 main/migrations/0047_utc_datetimes.py create mode 100644 news/migrations/0009_utc_datetimes.py (limited to 'news') diff --git a/main/migrations/0047_utc_datetimes.py b/main/migrations/0047_utc_datetimes.py new file mode 100644 index 00000000..83153b78 --- /dev/null +++ b/main/migrations/0047_utc_datetimes.py @@ -0,0 +1,180 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models +from django.utils.tzinfo import LocalTimezone + +def new_date(old_date, reverse=False): + if old_date is None: + return None + tz = LocalTimezone(old_date) + offset = tz.utcoffset(old_date) + if reverse: + offset = -offset + return old_date - offset + +class Migration(DataMigration): + + def forwards(self, orm): + all_pkgs = orm.Package.objects.all() + for package in all_pkgs: + # prevents full object updates + orm.Package.objects.filter(pk=package.pk).update( + last_update=new_date(package.last_update), + files_last_update=new_date(package.files_last_update), + flag_date=new_date(package.flag_date)) + # We could do todolists, but they just don't matter that much. + + def backwards(self, orm): + all_pkgs = orm.Package.objects.all() + for package in all_pkgs: + # prevents full object updates + orm.Package.objects.filter(pk=package.pk).update( + last_update=new_date(package.last_update, True), + files_last_update=new_date(package.files_last_update, True), + flag_date=new_date(package.flag_date, True)) + + 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'}) + }, + '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'}) + }, + 'main.arch': { + 'Meta': {'ordering': "['name']", 'object_name': 'Arch', 'db_table': "'arches'"}, + 'agnostic': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'main.donor': { + 'Meta': {'ordering': "['name']", 'object_name': 'Donor', 'db_table': "'donors'"}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'visible': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'main.package': { + 'Meta': {'ordering': "('pkgname',)", 'object_name': 'Package', 'db_table': "'packages'"}, + 'arch': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'to': "orm['main.Arch']"}), + 'build_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'compressed_size': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'epoch': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'files_last_update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'flag_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'installed_size': ('django.db.models.fields.BigIntegerField', [], {'null': 'True'}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}), + 'packager_str': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgbase': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgdesc': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'pkgname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'repo': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'packages'", 'to': "orm['main.Repo']"}), + 'url': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}) + }, + 'main.packagedepend': { + 'Meta': {'object_name': 'PackageDepend', 'db_table': "'package_depends'"}, + 'depname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'depvcmp': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'optional': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Package']"}) + }, + 'main.packagefile': { + 'Meta': {'object_name': 'PackageFile', 'db_table': "'package_files'"}, + 'directory': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'filename': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_directory': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Package']"}) + }, + 'main.repo': { + 'Meta': {'ordering': "['name']", 'object_name': 'Repo', 'db_table': "'repos'"}, + 'bugs_project': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'staging': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'svn_root': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'testing': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'main.signoff': { + 'Meta': {'object_name': 'Signoff'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'packager': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Package']"}), + 'pkgrel': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'pkgver': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'main.todolist': { + 'Meta': {'object_name': 'Todolist', 'db_table': "'todolists'"}, + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'date_added': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'description': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'main.todolistpkg': { + 'Meta': {'unique_together': "(('list', 'pkg'),)", 'object_name': 'TodolistPkg', 'db_table': "'todolist_pkgs'"}, + 'complete': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Todolist']"}), + 'pkg': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Package']"}) + }, + 'main.userprofile': { + 'Meta': {'object_name': 'UserProfile', 'db_table': "'user_profiles'"}, + 'alias': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'allowed_repos': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Repo']", 'symmetrical': 'False', 'blank': 'True'}), + 'favorite_distros': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'interests': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'languages': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'notify': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'occupation': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'other_contact': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'picture': ('django.db.models.fields.files.FileField', [], {'default': "'devs/silhouette.png'", 'max_length': '100'}), + 'public_email': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'roles': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'time_zone': ('django.db.models.fields.CharField', [], {'default': "'UTC'", 'max_length': '100'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'userprofile'", 'unique': 'True', 'to': "orm['auth.User']"}), + 'website': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), + 'yob': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) + } + } + + complete_apps = ['main'] diff --git a/news/migrations/0009_utc_datetimes.py b/news/migrations/0009_utc_datetimes.py new file mode 100644 index 00000000..6cddf783 --- /dev/null +++ b/news/migrations/0009_utc_datetimes.py @@ -0,0 +1,85 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models +from django.utils.tzinfo import LocalTimezone + +def new_date(old_date, reverse=False): + if old_date is None: + return None + tz = LocalTimezone(old_date) + offset = tz.utcoffset(old_date) + if reverse: + offset = -offset + return old_date - offset + +class Migration(DataMigration): + + def forwards(self, orm): + all_news = orm.News.objects.all().defer('content') + for news in all_news: + # prevents full object updates + orm.News.objects.filter(pk=news.pk).update( + postdate=new_date(news.postdate), + last_modified=new_date(news.last_modified)) + + def backwards(self, orm): + all_news = orm.News.objects.all().defer('content') + for news in all_news: + # prevents full object updates + orm.News.objects.filter(pk=news.pk).update( + postdate=new_date(news.postdate, True), + last_modified=new_date(news.last_modified, True)) + + 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'}) + }, + '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'}) + }, + 'news.news': { + 'Meta': {'ordering': "['-postdate']", 'object_name': 'News', 'db_table': "'news'"}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'news_author'", 'to': "orm['auth.User']"}), + 'content': ('django.db.models.fields.TextField', [], {}), + 'guid': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'postdate': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + } + } + + complete_apps = ['news'] -- cgit v1.2.3-54-g00ecf From 77842a6c76095277b024505708bf528d455b9c89 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Thu, 7 Apr 2011 16:52:52 -0500 Subject: Consolidate caching black magic Get the stuff used to retrieve and refresh the latest date values all in the same place, and make it a bit more beautiful by refactoring it all into a common set of methods. Signed-off-by: Dan McGee --- feeds.py | 44 +++++--------------------------------------- main/models.py | 7 +++---- main/utils.py | 34 +++++++++++++++++++++++++--------- news/models.py | 4 ++-- 4 files changed, 35 insertions(+), 54 deletions(-) (limited to 'news') diff --git a/feeds.py b/feeds.py index 0be12531..269d0a38 100644 --- a/feeds.py +++ b/feeds.py @@ -2,15 +2,13 @@ from django.contrib.sites.models import Site from django.contrib.syndication.views import Feed -from django.core.cache import cache from django.db.models import Q from django.utils.feedgenerator import Rss201rev2Feed from django.utils.hashcompat import md5_constructor from django.views.decorators.http import condition +from main.utils import retrieve_latest from main.models import Arch, Repo, Package -from main.utils import CACHE_TIMEOUT -from main.utils import CACHE_PACKAGE_KEY, CACHE_NEWS_KEY from news.models import News def check_for_unique_id(f): @@ -31,32 +29,14 @@ def write_items(self, handler): super(GuidNotPermalinkFeed, self).write_items(handler) -def retrieve_package_latest(): - # we could break this down based on the request url, but it would probably - # cost us more in query time to do so. - latest = cache.get(CACHE_PACKAGE_KEY) - if latest: - return latest - try: - latest = Package.objects.values('last_update').latest( - 'last_update')['last_update'] - # Using add means "don't overwrite anything in there". What could be in - # there is an explicit None value that our refresh signal set, which - # means we want to avoid race condition possibilities for a bit. - cache.add(CACHE_PACKAGE_KEY, latest, CACHE_TIMEOUT) - return latest - except Package.DoesNotExist: - pass - return None - def package_etag(request, *args, **kwargs): - latest = retrieve_package_latest() + latest = retrieve_latest(Package) if latest: return md5_constructor(str(kwargs) + str(latest)).hexdigest() return None def package_last_modified(request, *args, **kwargs): - return retrieve_package_latest() + return retrieve_latest(Package) class PackageFeed(Feed): feed_type = GuidNotPermalinkFeed @@ -125,28 +105,14 @@ def item_categories(self, item): return (item.repo.name, item.arch.name) -def retrieve_news_latest(): - latest = cache.get(CACHE_NEWS_KEY) - if latest: - return latest - try: - latest = News.objects.values('last_modified').latest( - 'last_modified')['last_modified'] - # same thoughts apply as in retrieve_package_latest - cache.add(CACHE_NEWS_KEY, latest, CACHE_TIMEOUT) - return latest - except News.DoesNotExist: - pass - return None - def news_etag(request, *args, **kwargs): - latest = retrieve_news_latest() + latest = retrieve_latest(News) if latest: return md5_constructor(str(latest)).hexdigest() return None def news_last_modified(request, *args, **kwargs): - return retrieve_news_latest() + return retrieve_latest(News) class NewsFeed(Feed): feed_type = GuidNotPermalinkFeed diff --git a/main/models.py b/main/models.py index 8d34731f..38120134 100644 --- a/main/models.py +++ b/main/models.py @@ -128,8 +128,7 @@ class Package(models.Model): class Meta: db_table = 'packages' ordering = ('pkgname',) - #get_latest_by = 'last_update' - #ordering = ('-last_update',) + get_latest_by = 'last_update' def __unicode__(self): return self.pkgname @@ -390,10 +389,10 @@ def set_todolist_fields(sender, **kwargs): todolist.date_added = datetime.utcnow() # connect signals needed to keep cache in line with reality -from main.utils import refresh_package_latest +from main.utils import refresh_latest from django.db.models.signals import pre_save, post_save -post_save.connect(refresh_package_latest, sender=Package, +post_save.connect(refresh_latest, sender=Package, dispatch_uid="main.models") pre_save.connect(set_todolist_fields, sender=Todolist, dispatch_uid="main.models") diff --git a/main/utils.py b/main/utils.py index d7681cb6..12d12503 100644 --- a/main/utils.py +++ b/main/utils.py @@ -6,10 +6,8 @@ from django.utils.hashcompat import md5_constructor CACHE_TIMEOUT = 1800 -INVALIDATE_TIMEOUT = 15 - -CACHE_PACKAGE_KEY = 'cache_package_latest' -CACHE_NEWS_KEY = 'cache_news_latest' +INVALIDATE_TIMEOUT = 10 +CACHE_LATEST_PREFIX = 'cache_latest_' def cache_function_key(func, args, kwargs): raw = [func.__name__, func.__module__, args, kwargs] @@ -53,16 +51,34 @@ def clear_cache_function(func, args, kwargs): # and hoops otherwise. The only thing currently using these keys is the feed # caching stuff. -def refresh_package_latest(**kwargs): +def refresh_latest(**kwargs): + '''A post_save signal handler to clear out the cached latest value for a + given model.''' + cache_key = CACHE_LATEST_PREFIX + kwargs['sender'].__name__ # We could delete the value, but that could open a race condition # where the new data wouldn't have been committed yet by the calling # thread. Instead, explicitly set it to None for a short amount of time. # Hopefully by the time it expires we will have committed, and the cache # will be valid again. See "Scaling Django" by Mike Malone, slide 30. - cache.set(CACHE_PACKAGE_KEY, None, INVALIDATE_TIMEOUT) + cache.set(cache_key, None, INVALIDATE_TIMEOUT) -def refresh_news_latest(**kwargs): - # same thoughts apply as in refresh_package_latest - cache.set(CACHE_NEWS_KEY, None, INVALIDATE_TIMEOUT) +def retrieve_latest(sender): + # we could break this down based on the request url, but it would probably + # cost us more in query time to do so. + cache_key = CACHE_LATEST_PREFIX + sender.__name__ + latest = cache.get(cache_key) + if latest: + return latest + try: + latest_by = sender._meta.get_latest_by + latest = sender.objects.values(latest_by).latest()[latest_by] + # Using add means "don't overwrite anything in there". What could be in + # there is an explicit None value that our refresh signal set, which + # means we want to avoid race condition possibilities for a bit. + cache.add(cache_key, latest, CACHE_TIMEOUT) + return latest + except sender.DoesNotExist: + pass + return None # vim: set ts=4 sw=4 et: diff --git a/news/models.py b/news/models.py index 17d51de9..5e467515 100644 --- a/news/models.py +++ b/news/models.py @@ -36,10 +36,10 @@ def set_news_fields(sender, **kwargs): now.strftime('%Y-%m-%d'), news.get_absolute_url()) # connect signals needed to keep cache in line with reality -from main.utils import refresh_news_latest +from main.utils import refresh_latest from django.db.models.signals import pre_save, post_save -post_save.connect(refresh_news_latest, sender=News, +post_save.connect(refresh_latest, sender=News, dispatch_uid="news.models") pre_save.connect(set_news_fields, sender=News, dispatch_uid="news.models") -- cgit v1.2.3-54-g00ecf From 6fe08cd68901e698f4a0741e177354a45c753b46 Mon Sep 17 00:00:00 2001 From: Dan McGee Date: Tue, 29 Mar 2011 17:39:59 -0500 Subject: Utilize Django 1.3 'on_delete' feature on several foreign keys The most important one here is PROTECT to keep people from making bone-headed plays and deleting an Arch or Repo and every package along with it. We can use this in a few other places, as well as some carefully placed SET_NULL indicators. Note that nothing here pushes deletion responsibilities down to the database, although that will probably happen in a future commit. Signed-off-by: Dan McGee --- main/models.py | 11 +++++++---- mirrors/models.py | 4 ++-- news/models.py | 3 ++- 3 files changed, 11 insertions(+), 7 deletions(-) (limited to 'news') diff --git a/main/models.py b/main/models.py index 38120134..772d85bb 100644 --- a/main/models.py +++ b/main/models.py @@ -104,8 +104,10 @@ class Meta: verbose_name_plural = 'repos' class Package(models.Model): - repo = models.ForeignKey(Repo, related_name="packages") - arch = models.ForeignKey(Arch, related_name="packages") + repo = models.ForeignKey(Repo, related_name="packages", + on_delete=models.PROTECT) + arch = models.ForeignKey(Arch, related_name="packages", + on_delete=models.PROTECT) pkgname = models.CharField(max_length=255, db_index=True) pkgbase = models.CharField(max_length=255, db_index=True) pkgver = models.CharField(max_length=255) @@ -121,7 +123,8 @@ class Package(models.Model): last_update = models.DateTimeField(null=True, blank=True) files_last_update = models.DateTimeField(null=True, blank=True) packager_str = models.CharField(max_length=255) - packager = models.ForeignKey(User, null=True) + packager = models.ForeignKey(User, null=True, + on_delete=models.SET_NULL) flag_date = models.DateTimeField(null=True) objects = PackageManager() @@ -348,7 +351,7 @@ class Meta: db_table = 'package_depends' class Todolist(models.Model): - creator = models.ForeignKey(User) + creator = models.ForeignKey(User, on_delete=models.PROTECT) name = models.CharField(max_length=255) description = models.TextField() date_added = models.DateTimeField(db_index=True) diff --git a/mirrors/models.py b/mirrors/models.py index dce17b5e..80808e2e 100644 --- a/mirrors/models.py +++ b/mirrors/models.py @@ -14,7 +14,7 @@ class Mirror(models.Model): name = models.CharField(max_length=255, unique=True) tier = models.SmallIntegerField(default=2, choices=TIER_CHOICES) - upstream = models.ForeignKey('self', null=True) + upstream = models.ForeignKey('self', null=True, on_delete=models.SET_NULL) country = models.CharField(max_length=255, db_index=True) admin_email = models.EmailField(max_length=255, blank=True) public = models.BooleanField(default=True) @@ -56,7 +56,7 @@ class Meta: class MirrorUrl(models.Model): url = models.CharField(max_length=255, unique=True) protocol = models.ForeignKey(MirrorProtocol, related_name="urls", - editable=False) + editable=False, on_delete=models.PROTECT) mirror = models.ForeignKey(Mirror, related_name="urls") has_ipv4 = models.BooleanField("IPv4 capable", default=True, editable=False) diff --git a/news/models.py b/news/models.py index 5e467515..33d958e0 100644 --- a/news/models.py +++ b/news/models.py @@ -6,7 +6,8 @@ class News(models.Model): slug = models.SlugField(max_length=255, unique=True) - author = models.ForeignKey(User, related_name='news_author') + author = models.ForeignKey(User, related_name='news_author', + on_delete=models.PROTECT) postdate = models.DateTimeField("post date", db_index=True) last_modified = models.DateTimeField(editable=False, db_index=True) title = models.CharField(max_length=255) -- cgit v1.2.3-54-g00ecf