From ec883ec62b20e7f39dfb3519fb9719c036bb009c Mon Sep 17 00:00:00 2001 From: Bryan Date: Thu, 9 Jan 2014 01:21:13 -0500 Subject: [PATCH] updating all models with Natural Keys for Django [dump|load]data for #89; adding some relevant unique constraints and cleaning up unicode formatting while I'm at it --- ...name_instructor_name__add_unique_course.py | 103 ++++++++++++ karmaworld/apps/courses/models.py | 156 ++++++++++++++++-- ...stream_link__add_unique_rawdocument_fp_.py | 115 +++++++++++++ karmaworld/apps/document_upload/models.py | 28 +++- .../0002_auto__add_unique_license_name.py | 29 ++++ karmaworld/apps/licenses/models.py | 18 +- ...link__add_unique_note_gdrive_url__add_u.py | 129 +++++++++++++++ karmaworld/apps/notes/models.py | 47 +++++- karmaworld/apps/users/models.py | 21 ++- 9 files changed, 619 insertions(+), 27 deletions(-) create mode 100644 karmaworld/apps/courses/migrations/0010_auto__del_unique_course_school_name_instructor_name__add_unique_course.py create mode 100644 karmaworld/apps/document_upload/migrations/0004_auto__add_unique_rawdocument_upstream_link__add_unique_rawdocument_fp_.py create mode 100644 karmaworld/apps/licenses/migrations/0002_auto__add_unique_license_name.py create mode 100644 karmaworld/apps/notes/migrations/0012_auto__add_unique_note_upstream_link__add_unique_note_gdrive_url__add_u.py diff --git a/karmaworld/apps/courses/migrations/0010_auto__del_unique_course_school_name_instructor_name__add_unique_course.py b/karmaworld/apps/courses/migrations/0010_auto__del_unique_course_school_name_instructor_name__add_unique_course.py new file mode 100644 index 0000000..fd71b85 --- /dev/null +++ b/karmaworld/apps/courses/migrations/0010_auto__del_unique_course_school_name_instructor_name__add_unique_course.py @@ -0,0 +1,103 @@ +# -*- coding: 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): + # Removing unique constraint on 'Course', fields ['school', 'name', 'instructor_name'] + db.delete_unique('courses_course', ['school_id', 'name', 'instructor_name']) + + # Adding unique constraint on 'Course', fields ['department', 'name'] + db.create_unique('courses_course', ['department_id', 'name']) + + # Adding unique constraint on 'Professor', fields ['name', 'email'] + db.create_unique('courses_professor', ['name', 'email']) + + # Adding unique constraint on 'School', fields ['usde_id'] + db.create_unique('courses_school', ['usde_id']) + + # Adding unique constraint on 'Department', fields ['school', 'name'] + db.create_unique('courses_department', ['school_id', 'name']) + + + def backwards(self, orm): + # Removing unique constraint on 'Department', fields ['school', 'name'] + db.delete_unique('courses_department', ['school_id', 'name']) + + # Removing unique constraint on 'School', fields ['usde_id'] + db.delete_unique('courses_school', ['usde_id']) + + # Removing unique constraint on 'Professor', fields ['name', 'email'] + db.delete_unique('courses_professor', ['name', 'email']) + + # Removing unique constraint on 'Course', fields ['department', 'name'] + db.delete_unique('courses_course', ['department_id', 'name']) + + # Adding unique constraint on 'Course', fields ['school', 'name', 'instructor_name'] + db.create_unique('courses_course', ['school_id', 'name', 'instructor_name']) + + + models = { + 'courses.course': { + 'Meta': {'ordering': "['-file_count', 'school', 'name']", 'unique_together': "(('name', 'department'),)", 'object_name': 'Course'}, + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'department': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['courses.Department']", 'null': 'True', 'blank': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'max_length': '511', 'null': 'True', 'blank': 'True'}), + 'file_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'flags': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instructor_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'instructor_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'school': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['courses.School']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '150', 'null': 'True'}), + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '511', 'null': 'True', 'blank': 'True'}) + }, + 'courses.department': { + 'Meta': {'unique_together': "(('name', 'school'),)", 'object_name': 'Department'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'school': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['courses.School']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '150', 'null': 'True'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '511', 'null': 'True', 'blank': 'True'}) + }, + 'courses.professor': { + 'Meta': {'unique_together': "(('name', 'email'),)", 'object_name': 'Professor'}, + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + 'courses.professoraffiliation': { + 'Meta': {'unique_together': "(('professor', 'department'),)", 'object_name': 'ProfessorAffiliation'}, + 'department': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['courses.Department']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'professor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['courses.Professor']"}) + }, + 'courses.professortaught': { + 'Meta': {'unique_together': "(('professor', 'course'),)", 'object_name': 'ProfessorTaught'}, + 'course': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['courses.Course']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'professor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['courses.Professor']"}) + }, + 'courses.school': { + 'Meta': {'ordering': "['-file_count', '-priority', 'name']", 'object_name': 'School'}, + 'alias': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'facebook_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'file_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'hashtag': ('django.db.models.fields.CharField', [], {'max_length': '16', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'priority': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '150', 'null': 'True'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '511', 'blank': 'True'}), + 'usde_id': ('django.db.models.fields.BigIntegerField', [], {'unique': 'True', 'null': 'True', 'blank': 'True'}) + } + } + + complete_apps = ['courses'] \ No newline at end of file diff --git a/karmaworld/apps/courses/models.py b/karmaworld/apps/courses/models.py index 3fdabfd..4543a07 100644 --- a/karmaworld/apps/courses/models.py +++ b/karmaworld/apps/courses/models.py @@ -15,8 +15,19 @@ from django.template import defaultfilters from karmaworld.settings.manual_unique_together import auto_add_check_unique_together +class SchoolManager(models.Manager): + """ Handle restoring data. """ + def get_by_natural_key(self, usde_id): + """ + Return a School defined by USDE number. + """ + return self.get(usde_id=usde_id) + + class School(models.Model): """ A grouping that contains many courses """ + objects = SchoolManager() + name = models.CharField(max_length=255) slug = models.SlugField(max_length=150, null=True) location = models.CharField(max_length=255, blank=True, null=True) @@ -24,7 +35,7 @@ class School(models.Model): # Facebook keeps a unique identifier for all schools facebook_id = models.BigIntegerField(blank=True, null=True) # United States Department of Education institution_id - usde_id = models.BigIntegerField(blank=True, null=True) + usde_id = models.BigIntegerField(blank=True, null=True, unique=True) file_count = models.IntegerField(default=0) priority = models.BooleanField(default=0) alias = models.CharField(max_length=255, null=True, blank=True) @@ -34,8 +45,16 @@ class School(models.Model): """ Sort School by file_count descending, name abc=> """ ordering = ['-file_count','-priority', 'name'] + def natural_key(self): + """ + A School is uniquely defined by USDE number. + + Name should be unique, but there are some dupes in the DB. + """ + return (self.usde_id,) + def __unicode__(self): - return self.name + return u'School {0}: {1}'.format(self.usde_id, self.name) def save(self, *args, **kwargs): """ Save school and generate a slug if one doesn't exist """ @@ -55,15 +74,41 @@ class School(models.Model): self.save() +class DepartmentManager(models.Manager): + """ Handle restoring data. """ + def get_by_natural_key(self, name, school): + """ + Return a Department defined by its name and school. + """ + return self.get(name=name, school=school) + + class Department(models.Model): """ Department within a School. """ + objects = DepartmentManager() + name = models.CharField(max_length=255) school = models.ForeignKey(School) # Should this be optional ever? slug = models.SlugField(max_length=150, null=True) url = models.URLField(max_length=511, blank=True, null=True) + class Meta: + """ + The same department name might exist across schools, but only once + per school. + """ + unique_together = ('name', 'school',) + def __unicode__(self): - return self.name + return u'Department: {0} at {1}'.format(self.name, unicode(self.school)) + + def natural_key(self): + """ + A Department is uniquely defined by its school and name. + """ + return (self.name, self.school.natural_key()) + # Requires School to be dumped first + natural_key.dependencies = ['courses.school'] def save(self, *args, **kwargs): """ Save department and generate a slug if one doesn't exist """ @@ -72,35 +117,92 @@ class Department(models.Model): super(Department, self).save(*args, **kwargs) +class ProfessorManager(models.Manager): + """ Handle restoring data. """ + def get_by_natural_key(self, name, email): + """ + Return a Professor defined by name and email address. + """ + return self.get(name=name,email=email) + + class Professor(models.Model): """ Track professors for courses. """ + objects = ProfessorManager() + name = models.CharField(max_length=255) email = models.EmailField(blank=True, null=True) + class Meta: + """ + email should be unique, but some professors have no email address + in the database. For those cases, the name must be appended for + uniqueness. + """ + unique_together = ('name', 'email',) + def __unicode__(self): - return u'Professor: {0}'.format(self.name) + return u'Professor: {0} ({1})'.format(self.name, self.email) + + def natural_key(self): + """ + A Professor is uniquely defined by his/her name and email. + """ + return (self.name,self.email) + + +class ProfessorAffiliationManager(models.Manager): + """ Handle restoring data. """ + def get_by_natural_key(self, prof, dept): + """ + Return a ProfessorAffiliation defined by prof and department. + """ + return self.get(professor=prof,department=dept) class ProfessorAffiliation(models.Model): """ Track professors for departments. (many-to-many) """ - professor = models.ForeignKey(Professor) + objects = ProfessorAffiliationManager() + + professor = models.ForeignKey(Professor) department = models.ForeignKey(Department) def __unicode__(self): - return u'Professor {0} working for {1}'.format(self.professor.name, self.department.name) + return u'Professor {0} working for {1}'.format(unicode(self.professor), unicode(self.department)) class Meta: - # many-to-many across both fields, - # but (prof, dept) as a tuple should only appear once. + """ + Many-to-many across both professor and department. + However, (prof, dept) as a tuple should only appear once. + """ unique_together = ('professor', 'department',) + def natural_key(self): + """ + A ProfessorAffiliation is uniquely defined by the prof and department + """ + return (self.professor.natural_key(), self.department.natural_key()) + # Requires dependencies to be dumped first + natural_key.dependencies = ['courses.professor','courses.department'] + + +class CourseManager(models.Manager): + """ Handle restoring data. """ + def get_by_natural_key(self, name, dept): + """ + Return a Course defined by name and department. + """ + return self.get(name=name,department=dept) + class Course(models.Model): """ First class object that contains many notes.Note objects """ + objects = CourseManager() + # Core metadata name = models.CharField(max_length=255) slug = models.SlugField(max_length=150, null=True) @@ -125,15 +227,22 @@ class Course(models.Model): # Number of times this course has been flagged as abusive/spam. flags = models.IntegerField(default=0,null=False) - class Meta: ordering = ['-file_count', 'school', 'name'] - unique_together = ('school', 'name', 'instructor_name') + unique_together = ('name', 'department') verbose_name = 'course' verbose_name_plural = 'courses' def __unicode__(self): - return u"{0}: {1}".format(self.name, self.school) + return u"Course {0} in {1}".format(self.name, unicode(self.department)) + + def natural_key(self): + """ + A Course is uniquely defined by its name and the department it is in. + """ + return (self.name, self.department.natural_key()) + # Requires dependencies to be dumped first + natural_key.dependencies = ['courses.department'] def get_absolute_url(self): """ return url based on school slug and self slug """ @@ -160,24 +269,45 @@ class Course(models.Model): self.save() +class ProfessorTaughtManager(models.Manager): + """ Handle restoring data. """ + def get_by_natural_key(self, prof, course): + """ + Return a ProfessorTaught defined by professor and course. + """ + return self.get(professor=prof, course=course) + + class ProfessorTaught(models.Model): """ Track professors teaching courses. (many-to-many) """ + objects = ProfessorTaughtManager() + professor = models.ForeignKey(Professor) - course = models.ForeignKey(Course) + course = models.ForeignKey(Course) def __unicode__(self): - return u'Professor {0} taught {1}'.format(self.professor.name, self.course.name) + return u'Professor {0} taught {1}'.format(unicode(self.professor), unicode(self.course)) class Meta: # many-to-many across both fields, # but (prof, course) as a tuple should only appear once. unique_together = ('professor', 'course',) + def natural_key(self): + """ + A ProfessorTaught is uniquely defined by the prof and course. + """ + return (self.professor.natural_key(), self.course.natural_key()) + # Requires dependencies to be dumped first + natural_key.dependencies = ['courses.professor','courses.course'] + # Enforce unique constraints even when we're using a database like # SQLite that doesn't understand them auto_add_check_unique_together(Course) +auto_add_check_unique_together(Department) +auto_add_check_unique_together(Professor) auto_add_check_unique_together(ProfessorAffiliation) auto_add_check_unique_together(ProfessorTaught) diff --git a/karmaworld/apps/document_upload/migrations/0004_auto__add_unique_rawdocument_upstream_link__add_unique_rawdocument_fp_.py b/karmaworld/apps/document_upload/migrations/0004_auto__add_unique_rawdocument_upstream_link__add_unique_rawdocument_fp_.py new file mode 100644 index 0000000..d4c28b2 --- /dev/null +++ b/karmaworld/apps/document_upload/migrations/0004_auto__add_unique_rawdocument_upstream_link__add_unique_rawdocument_fp_.py @@ -0,0 +1,115 @@ +# -*- coding: 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 unique constraint on 'RawDocument', fields ['upstream_link'] + db.create_unique('document_upload_rawdocument', ['upstream_link']) + + # Adding unique constraint on 'RawDocument', fields ['fp_file', 'upstream_link'] + db.create_unique('document_upload_rawdocument', ['fp_file', 'upstream_link']) + + + def backwards(self, orm): + # Removing unique constraint on 'RawDocument', fields ['fp_file', 'upstream_link'] + db.delete_unique('document_upload_rawdocument', ['fp_file', 'upstream_link']) + + # Removing unique constraint on 'RawDocument', fields ['upstream_link'] + db.delete_unique('document_upload_rawdocument', ['upstream_link']) + + + models = { + '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'}) + }, + 'courses.course': { + 'Meta': {'ordering': "['-file_count', 'school', 'name']", 'unique_together': "(('name', 'department'),)", 'object_name': 'Course'}, + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'department': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['courses.Department']", 'null': 'True', 'blank': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'max_length': '511', 'null': 'True', 'blank': 'True'}), + 'file_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'flags': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instructor_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'instructor_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'school': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['courses.School']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '150', 'null': 'True'}), + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '511', 'null': 'True', 'blank': 'True'}) + }, + 'courses.department': { + 'Meta': {'unique_together': "(('name', 'school'),)", 'object_name': 'Department'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'school': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['courses.School']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '150', 'null': 'True'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '511', 'null': 'True', 'blank': 'True'}) + }, + 'courses.school': { + 'Meta': {'ordering': "['-file_count', '-priority', 'name']", 'object_name': 'School'}, + 'alias': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'facebook_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'file_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'hashtag': ('django.db.models.fields.CharField', [], {'max_length': '16', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'priority': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '150', 'null': 'True'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '511', 'blank': 'True'}), + 'usde_id': ('django.db.models.fields.BigIntegerField', [], {'unique': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'document_upload.rawdocument': { + 'Meta': {'ordering': "['-uploaded_at']", 'unique_together': "(('fp_file', 'upstream_link'),)", 'object_name': 'RawDocument'}, + 'course': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['courses.Course']"}), + 'fp_file': ('django_filepicker.models.FPFileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}), + 'is_hidden': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_processed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'license': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['licenses.License']", 'null': 'True', 'blank': 'True'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True'}), + 'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow', 'null': 'True'}), + 'upstream_link': ('django.db.models.fields.URLField', [], {'max_length': '1024', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['users.KarmaUser']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}) + }, + 'licenses.license': { + 'Meta': {'object_name': 'License'}, + 'html': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}) + }, + 'taggit.tag': { + 'Meta': {'ordering': "['namespace', 'name']", 'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'namespace': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'}) + }, + 'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"}) + }, + 'users.karmauser': { + 'Meta': {'object_name': 'KarmaUser'}, + 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + } + } + + complete_apps = ['document_upload'] \ No newline at end of file diff --git a/karmaworld/apps/document_upload/models.py b/karmaworld/apps/document_upload/models.py index 8a1d9b7..3034ce4 100644 --- a/karmaworld/apps/document_upload/models.py +++ b/karmaworld/apps/document_upload/models.py @@ -10,18 +10,39 @@ import django_filepicker from karmaworld.apps.notes.models import Document from karmaworld.apps.notes.models import Note from karmaworld.apps.document_upload import tasks +from karmaworld.settings.manual_unique_together import auto_add_check_unique_together + + +class RawDocumentManager(models.Manager): + """ Handle restoring data. """ + def get_by_natural_key(self, fp_file, upstream_link): + """ + Return a RawDocument defined by its Filepicker and upstream URLs. + """ + return self.get(fp_file=fp_file,upstream_link=upstream_link) class RawDocument(Document): + objects = RawDocumentManager() + is_processed = models.BooleanField(default=False) class Meta: """ Sort files most recent first """ ordering = ['-uploaded_at'] - + unique_together = ('fp_file', 'upstream_link') def __unicode__(self): - return u"{0} @ {1}".format(self.ip, self.uploaded_at) + return u"RawDocument at {0} (from {1})".format(self.fp_file, self.upstream_link) + + def natural_key(self): + """ + A RawDocument is uniquely defined by both the Filepicker link and the + upstream link. The Filepicker link should be unique by itself, but + it may be null in the database, so the upstream link component should + resolve those cases. + """ + return (self.fp_file, self.upstream_link) def convert_to_note(self): """ polymorph this object into a note.models.Note object """ @@ -48,3 +69,6 @@ class RawDocument(Document): super(RawDocument, self).save(*args, **kwargs) if not self.is_processed: tasks.process_raw_document.delay(self) + + +auto_add_check_unique_together(RawDocument) diff --git a/karmaworld/apps/licenses/migrations/0002_auto__add_unique_license_name.py b/karmaworld/apps/licenses/migrations/0002_auto__add_unique_license_name.py new file mode 100644 index 0000000..b0b285f --- /dev/null +++ b/karmaworld/apps/licenses/migrations/0002_auto__add_unique_license_name.py @@ -0,0 +1,29 @@ +# -*- coding: 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 unique constraint on 'License', fields ['name'] + db.create_unique('licenses_license', ['name']) + + + def backwards(self, orm): + # Removing unique constraint on 'License', fields ['name'] + db.delete_unique('licenses_license', ['name']) + + + models = { + 'licenses.license': { + 'Meta': {'object_name': 'License'}, + 'html': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}) + } + } + + complete_apps = ['licenses'] \ No newline at end of file diff --git a/karmaworld/apps/licenses/models.py b/karmaworld/apps/licenses/models.py index 603690d..81753c9 100644 --- a/karmaworld/apps/licenses/models.py +++ b/karmaworld/apps/licenses/models.py @@ -8,14 +8,30 @@ from django.db import models +class LicenseManager(models.Manager): + """ Handle restoring data. """ + def get_by_natural_key(self, name): + """ + Return a License defined by its brief name. + """ + return self.get(name=name) + + class License(models.Model): """ Track licenses for notes which are different from the default license assumed for the site. """ + objects = LicenseManager() - name = models.CharField(max_length=80) + name = models.CharField(max_length=80,unique=True) html = models.TextField() def __unicode__(self): return u'License: {0}'.format(self.name) + + def natural_key(self): + """ + A License is uniquely defined by its brief name. + """ + return (self.name,) diff --git a/karmaworld/apps/notes/migrations/0012_auto__add_unique_note_upstream_link__add_unique_note_gdrive_url__add_u.py b/karmaworld/apps/notes/migrations/0012_auto__add_unique_note_upstream_link__add_unique_note_gdrive_url__add_u.py new file mode 100644 index 0000000..26b2315 --- /dev/null +++ b/karmaworld/apps/notes/migrations/0012_auto__add_unique_note_upstream_link__add_unique_note_gdrive_url__add_u.py @@ -0,0 +1,129 @@ +# -*- coding: 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 unique constraint on 'Note', fields ['upstream_link'] + db.create_unique('notes_note', ['upstream_link']) + + # Adding unique constraint on 'Note', fields ['gdrive_url'] + db.create_unique('notes_note', ['gdrive_url']) + + # Adding unique constraint on 'Note', fields ['fp_file', 'upstream_link'] + db.create_unique('notes_note', ['fp_file', 'upstream_link']) + + + def backwards(self, orm): + # Removing unique constraint on 'Note', fields ['fp_file', 'upstream_link'] + db.delete_unique('notes_note', ['fp_file', 'upstream_link']) + + # Removing unique constraint on 'Note', fields ['gdrive_url'] + db.delete_unique('notes_note', ['gdrive_url']) + + # Removing unique constraint on 'Note', fields ['upstream_link'] + db.delete_unique('notes_note', ['upstream_link']) + + + models = { + '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'}) + }, + 'courses.course': { + 'Meta': {'ordering': "['-file_count', 'school', 'name']", 'unique_together': "(('name', 'department'),)", 'object_name': 'Course'}, + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'department': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['courses.Department']", 'null': 'True', 'blank': 'True'}), + 'desc': ('django.db.models.fields.TextField', [], {'max_length': '511', 'null': 'True', 'blank': 'True'}), + 'file_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'flags': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instructor_email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}), + 'instructor_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'school': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['courses.School']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '150', 'null': 'True'}), + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '511', 'null': 'True', 'blank': 'True'}) + }, + 'courses.department': { + 'Meta': {'unique_together': "(('name', 'school'),)", 'object_name': 'Department'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'school': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['courses.School']"}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '150', 'null': 'True'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '511', 'null': 'True', 'blank': 'True'}) + }, + 'courses.school': { + 'Meta': {'ordering': "['-file_count', '-priority', 'name']", 'object_name': 'School'}, + 'alias': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'facebook_id': ('django.db.models.fields.BigIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'file_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'hashtag': ('django.db.models.fields.CharField', [], {'max_length': '16', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'priority': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '150', 'null': 'True'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '511', 'blank': 'True'}), + 'usde_id': ('django.db.models.fields.BigIntegerField', [], {'unique': 'True', 'null': 'True', 'blank': 'True'}) + }, + 'licenses.license': { + 'Meta': {'object_name': 'License'}, + 'html': ('django.db.models.fields.TextField', [], {}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}) + }, + 'notes.note': { + 'Meta': {'unique_together': "(('fp_file', 'upstream_link'),)", 'object_name': 'Note'}, + 'course': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['courses.Course']"}), + 'file_type': ('django.db.models.fields.CharField', [], {'default': "'???'", 'max_length': '15', 'null': 'True', 'blank': 'True'}), + 'flags': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'fp_file': ('django_filepicker.models.FPFileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'gdrive_url': ('django.db.models.fields.URLField', [], {'max_length': '1024', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'html': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}), + 'is_hidden': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'license': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['licenses.License']", 'null': 'True', 'blank': 'True'}), + 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'pdf_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'thanks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'tweeted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow', 'null': 'True'}), + 'upstream_link': ('django.db.models.fields.URLField', [], {'max_length': '1024', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['users.KarmaUser']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'year': ('django.db.models.fields.IntegerField', [], {'default': '2014', 'null': 'True', 'blank': 'True'}) + }, + 'taggit.tag': { + 'Meta': {'ordering': "['namespace', 'name']", 'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'namespace': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '100'}) + }, + 'taggit.taggeditem': { + 'Meta': {'object_name': 'TaggedItem'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_tagged_items'", 'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}), + 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'taggit_taggeditem_items'", 'to': "orm['taggit.Tag']"}) + }, + 'users.karmauser': { + 'Meta': {'object_name': 'KarmaUser'}, + 'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + } + } + + complete_apps = ['notes'] \ No newline at end of file diff --git a/karmaworld/apps/notes/models.py b/karmaworld/apps/notes/models.py index 2797883..57266b7 100644 --- a/karmaworld/apps/notes/models.py +++ b/karmaworld/apps/notes/models.py @@ -26,9 +26,12 @@ from taggit.managers import TaggableManager from karmaworld.apps.courses.models import Course from karmaworld.apps.licenses.models import License from karmaworld.apps.notes.search import SearchIndex +from karmaworld.settings.manual_unique_together import auto_add_check_unique_together + fs = FileSystemStorage(location=settings.MEDIA_ROOT) + def _choose_upload_to(instance, filename): # /school/course/year/month/day return u"{school}/{course}/{year}/{month}/{day}".format( @@ -38,9 +41,10 @@ def _choose_upload_to(instance, filename): month=instance.uploaded_at.month, day=instance.uploaded_at.day) + class Document(models.Model): - """ An Abstract Base Class representing a document - intended to be subclassed + """ + An Abstract Base Class representing a document intended to be subclassed. """ course = models.ForeignKey(Course) tags = TaggableManager(blank=True) @@ -51,7 +55,7 @@ class Document(models.Model): license = models.ForeignKey(License, blank=True, null=True) # provide an upstream file link - upstream_link = models.URLField(max_length=1024, blank=True, null=True) + upstream_link = models.URLField(max_length=1024, blank=True, null=True, unique=True) # metadata relevant to the Upload process user = models.ForeignKey('users.KarmaUser', blank=True, null=True, on_delete=SET_NULL) @@ -75,9 +79,6 @@ class Document(models.Model): abstract = True ordering = ['-uploaded_at'] - def __unicode__(self): - return u"Document: {1} -- {2}".format(self.name, self.uploaded_at) - def _generate_unique_slug(self): """ generate a unique slug based on name and uploaded_at """ _slug = defaultfilters.slugify(self.name) @@ -123,9 +124,22 @@ class Document(models.Model): self._generate_unique_slug() super(Document, self).save(*args, **kwargs) + +class NoteManager(models.Manager): + """ Handle restoring data. """ + def get_by_natural_key(self, fp_file, upstream_link): + """ + Return a Note defined by its Filepicker and upstream URLs. + """ + return self.get(fp_file=fp_file,upstream_link=upstream_link) + + class Note(Document): - """ A django model representing an uploaded file and associated metadata. + """ + A django model representing an uploaded file and associated metadata. """ + objects = NoteManager() + # FIXME: refactor file choices after FP.io integration UNKNOWN_FILE = '???' FILE_TYPE_CHOICES = ( @@ -143,7 +157,7 @@ class Note(Document): blank=True, null=True) # Cache the Google drive file link - gdrive_url = models.URLField(max_length=1024, blank=True, null=True) + gdrive_url = models.URLField(max_length=1024, blank=True, null=True, unique=True) # Upload files to MEDIA_ROOT/notes/YEAR/MONTH/DAY, 2012/10/30/filename pdf_file = models.FileField( @@ -167,9 +181,21 @@ class Note(Document): tweeted = models.BooleanField(default=False) thanks = models.PositiveIntegerField(default=0) + class Meta: + unique_together = ('fp_file', 'upstream_link') + def __unicode__(self): - return u"Note: {0} {1} -- {2}".format(self.file_type, self.name, self.uploaded_at) + return u"Note at {0} (from {1})".format(self.fp_file, self.upstream_link) + def natural_key(self): + """ + A Note is uniquely defined by both the Filepicker link and the upstream + link. The Filepicker link should be unique by itself, but it may be + null in the database, so the upstream link component should resolve + those cases. + """ + # gdrive_url might also fit the bill? + return (self.fp_file, self.upstream_link) def get_absolute_url(self): """ Resolve note url, use 'note' route and slug if slug @@ -218,6 +244,9 @@ class Note(Document): super(Note, self).save(*args, **kwargs) +auto_add_check_unique_together(Note) + + def update_note_counts(note_instance): try: # test if the course still exists, or if this is a cascade delete. diff --git a/karmaworld/apps/users/models.py b/karmaworld/apps/users/models.py index 4539aa3..27aef53 100644 --- a/karmaworld/apps/users/models.py +++ b/karmaworld/apps/users/models.py @@ -5,9 +5,26 @@ from django.contrib import admin from django.db import models + +class KarmaUserManager(models.Manager): + """ Handle restoring data. """ + def get_by_natural_key(self, email): + """ + Return a KarmaUser defined by his/her email address. + """ + return self.get(email=email) + + class KarmaUser(models.Model): - email = models.EmailField(blank=False, null=False, unique=True) + objects = KarmaUserManager() + + email = models.EmailField(blank=False, null=False, unique=True) def __unicode__(self): - return u'{e}'.format(e=self.email) + return u'KarmaUser: {0}'.format(self.email) + def natural_key(self): + """ + A KarmaUser is uniquely defined by his/her email address. + """ + return (self.email,) -- 2.25.1