3 # Copyright (C) 2012 FinalsClub Foundation
6 Models for the notes django app.
7 Contains only the minimum for handling files and their representation
10 from django.db.models.signals import post_save, post_delete
11 from django.dispatch import receiver
15 from django.conf import settings
16 from django.core.files import File
17 from django.core.files.storage import FileSystemStorage
18 from django.db import models
19 from django.template import defaultfilters
20 import django_filepicker
21 from lxml.html import fromstring, tostring
22 from oauth2client.client import Credentials
23 from taggit.managers import TaggableManager
25 from karmaworld.apps.courses.models import Course
26 from karmaworld.apps.users.models import KarmaUser
29 from secrets.drive import GOOGLE_USER
31 GOOGLE_USER = u'admin@karmanotes.org'
33 fs = FileSystemStorage(location=settings.MEDIA_ROOT)
35 def _choose_upload_to(instance, filename):
36 # /school/course/year/month/day
37 return u"{school}/{course}/{year}/{month}/{day}".format(
38 school=instance.course.school.slug,
39 course=instance.course.slug,
40 year=instance.uploaded_at.year,
41 month=instance.uploaded_at.month,
42 day=instance.uploaded_at.day)
44 class Document(models.Model):
45 """ An Abstract Base Class representing a document
46 intended to be subclassed
49 course = models.ForeignKey(Course)
50 tags = TaggableManager(blank=True)
51 name = models.CharField(max_length=255, blank=True, null=True)
52 slug = models.SlugField(max_length=255, null=True)
54 # metadata relevant to the Upload process
55 user = models.ForeignKey('users.KarmaUser', null=True)
56 ip = models.IPAddressField(blank=True, null=True,
57 help_text=u"IP address of the uploader")
58 uploaded_at = models.DateTimeField(null=True, default=datetime.datetime.utcnow)
61 # if True, NEVER show this file
62 # WARNING: This may throw an error on migration
63 is_hidden = models.BooleanField(default=False)
65 fp_file = django_filepicker.models.FPFileField(
66 upload_to=_choose_upload_to,
68 null=True, blank=True,
69 help_text=u"An uploaded file reference from Filepicker.io")
70 mimetype = models.CharField(max_length=255, blank=True, null=True)
74 ordering = ['-uploaded_at']
76 def __unicode__(self):
77 return u"Document: {1} -- {2}".format(self.name, self.uploaded_at)
79 def _generate_unique_slug(self):
80 """ generate a unique slug based on name and uploaded_at """
81 _slug = defaultfilters.slugify(self.name)
82 klass = self.__class__
83 collision = klass.objects.filter(slug=_slug)
85 _slug = u"{0}-{1}-{2}-{3}".format(
86 _slug, self.uploaded_at.month,
87 self.uploaded_at.day, self.uploaded_at.microsecond)
91 """ Downloads the file from filepicker.io and returns a
92 Django File wrapper object """
93 # clean up any old downloads that are still hanging around
94 if hasattr(self, 'tempfile'):
96 delattr(self, 'tempfile')
98 if hasattr(self, 'filename'):
99 # the file might have been moved in the meantime so
101 if os.path.exists(self.filename):
102 os.remove(self.filename)
103 delattr(self, 'filename')
105 # The temporary file will be created in a directory set by the
106 # environment (TEMP_DIR, TEMP or TMP)
107 self.filename, header = urllib.urlretrieve(self.fp_file.name)
108 name = os.path.basename(self.filename)
109 disposition = header.get('Content-Disposition')
111 name = disposition.rpartition("filename=")[2].strip('" ')
112 filename = header.get('X-File-Name')
116 self.tempfile = open(self.filename, 'r')
117 return File(self.tempfile, name=name)
119 def save(self, *args, **kwargs):
120 if self.name and not self.slug:
121 self._generate_unique_slug()
122 super(Document, self).save(*args, **kwargs)
124 class Note(Document):
125 """ A django model representing an uploaded file and associated metadata.
127 # FIXME: refactor file choices after FP.io integration
129 FILE_TYPE_CHOICES = (
130 ('doc', 'MS Word compatible file (.doc, .docx, .rtf, .odf)'),
131 ('img', 'Scan or picture of notes'),
133 ('ppt', 'Powerpoint'),
135 (UNKNOWN_FILE, 'Unknown file'),
138 file_type = models.CharField(max_length=15, \
139 choices=FILE_TYPE_CHOICES, \
140 default=UNKNOWN_FILE, \
141 blank=True, null=True)
143 # Upload files to MEDIA_ROOT/notes/YEAR/MONTH/DAY, 2012/10/30/filename
144 pdf_file = models.FileField( \
146 upload_to="notes/%Y/%m/%d/",\
147 blank=True, null=True)
148 # No longer keeping a local copy backed by django
149 note_file = models.FileField( \
151 upload_to="notes/%Y/%m/%d/",\
152 blank=True, null=True)
155 embed_url = models.URLField(max_length=1024, blank=True, null=True)
156 download_url = models.URLField(max_length=1024, blank=True, null=True)
158 # Generated by Google Drive by saved locally
159 html = models.TextField(blank=True, null=True)
160 text = models.TextField(blank=True, null=True)
163 # not using, but keeping old data
164 year = models.IntegerField(blank=True, null=True,\
165 default=datetime.datetime.utcnow().year)
166 desc = models.TextField(max_length=511, blank=True, null=True)
168 is_flagged = models.BooleanField(default=False)
169 is_moderated = models.BooleanField(default=False)
172 def __unicode__(self):
173 return u"Note: {0} {1} -- {2}".format(self.file_type, self.name, self.uploaded_at)
176 def get_absolute_url(self):
177 """ Resolve note url, use 'note' route and slug if slug
178 otherwise use note.id
180 if self.slug is not None:
181 # return a url ending in slug
182 return u"/{0}/{1}/{2}".format(self.course.school.slug, self.course.slug, self.slug)
184 # return a url ending in id
185 return u"/{0}/{1}/{2}".format(self.course.school.slug, self.course.slug, self.id)
187 def sanitize_html(self, save=True):
188 """ if self contains html, find all <a> tags and add target=_blank
190 returns True/False on succ/fail and error or count
192 # build a tag sanitizer
193 def add_attribute_target(tag):
194 tag.attrib['target'] = '_blank'
196 # if no html, return false
198 return False, "Note has no html"
200 _html = fromstring(self.html)
201 a_tags = _html.findall('.//a') # recursively find all a tags in document tree
202 # if there are a tags
204 #apply the add attribute function
205 map(add_attribute_target, a_tags)
206 self.html = tostring(_html)
209 return True, len(a_tags)
211 def _update_parent_updated_at(self):
212 """ update the parent Course.updated_at model
213 with the latest uploaded_at """
214 self.course.updated_at = self.uploaded_at
217 def save(self, *args, **kwargs):
218 if self.uploaded_at and self.uploaded_at > self.course.updated_at:
219 self._update_parent_updated_at()
220 super(Note, self).save(*args, **kwargs)
223 def update_note_counts(note_instance):
224 note_instance.course.update_note_count()
225 note_instance.course.school.update_note_count()
227 @receiver(post_save, sender=Note, weak=False)
228 def note_receiver(sender, **kwargs):
229 if kwargs['created']:
230 update_note_counts(kwargs['instance'])
232 @receiver(post_delete, sender=Note, weak=False)
233 def note_receiver(sender, **kwargs):
234 update_note_counts(kwargs['instance'])
237 class DriveAuth(models.Model):
238 """ stored google drive authentication and refresh token
239 used for interacting with google drive """
241 email = models.EmailField(default=GOOGLE_USER)
242 credentials = models.TextField() # JSON of Oauth2Credential object
243 stored_at = models.DateTimeField(auto_now=True)
247 def get(email=GOOGLE_USER):
248 """ Staticmethod for getting the singleton DriveAuth object """
249 # FIXME: this is untested
250 return DriveAuth.objects.filter(email=email).reverse()[0]
253 def store(self, creds):
254 """ Transform an existing credentials object to a db serialized """
255 self.email = creds.id_token['email']
256 self.credentials = creds.to_json()
260 def transform_to_cred(self):
261 """ take stored credentials and produce a Credentials object """
262 return Credentials.new_from_json(self.credentials)
265 def __unicode__(self):
266 return u'Gdrive auth for %s created/updated at %s' % \
267 (self.email, self.stored_at)