3 # Copyright (C) 2012 FinalsClub Foundation
6 Models for the notes django app.
7 Contains only the minimum for handling files and their representation
13 from django.conf import settings
14 from django.core.files import File
15 from django.core.files.storage import FileSystemStorage
16 from django.db import models
17 from django.template import defaultfilters
18 import django_filepicker
19 from lxml.html import fromstring, tostring
20 from oauth2client.client import Credentials
21 from taggit.managers import TaggableManager
23 from karmaworld.apps.courses.models import Course
26 from secrets.drive import GOOGLE_USER
28 GOOGLE_USER = u'admin@karmanotes.org'
30 fs = FileSystemStorage(location=settings.MEDIA_ROOT)
32 def _choose_upload_to(instance, filename):
33 # /school/course/year/month/day
34 return u"{school}/{course}/{year}/{month}/{day}".format(
35 school=instance.course.school.slug,
36 course=instance.course.slug,
37 year=instance.uploaded_at.year,
38 month=instance.uploaded_at.month,
39 day=instance.uploaded_at.day)
41 class Document(models.Model):
42 """ An Abstract Base Class representing a document
43 intended to be subclassed
46 course = models.ForeignKey(Course)
47 tags = TaggableManager(blank=True)
48 name = models.CharField(max_length=255, blank=True, null=True)
49 slug = models.SlugField(max_length=255, null=True)
51 # metadata relevant to the Upload process
52 ip = models.IPAddressField(blank=True, null=True,
53 help_text=u"IP address of the uploader")
54 uploaded_at = models.DateTimeField(null=True, default=datetime.datetime.utcnow)
57 # if True, NEVER show this file
58 # WARNING: This may throw an error on migration
59 is_hidden = models.BooleanField(default=False)
61 fp_file = django_filepicker.models.FPFileField(
62 upload_to=_choose_upload_to,
64 null=True, blank=True,
65 help_text=u"An uploaded file reference from Filepicker.io")
66 mimetype = models.CharField(max_length=255, blank=True, null=True)
70 ordering = ['-uploaded_at']
72 def __unicode__(self):
73 return u"Document: {1} -- {2}".format(self.name, self.uploaded_at)
75 def _generate_unique_slug(self):
76 """ generate a unique slug based on name and uploaded_at """
77 _slug = defaultfilters.slugify(self.name)
78 klass = self.__class__
79 collision = klass.objects.filter(slug=_slug)
81 _slug = u"{0}-{1}-{2}-{3}".format(
82 _slug, self.uploaded_at.month,
83 self.uploaded_at.day, self.uploaded_at.microsecond)
87 """ Downloads the file from filepicker.io and returns a
88 Django File wrapper object """
89 # clean up any old downloads that are still hanging around
90 if hasattr(self, 'tempfile'):
92 delattr(self, 'tempfile')
94 if hasattr(self, 'filename'):
95 # the file might have been moved in the meantime so
97 if os.path.exists(self.filename):
98 os.remove(self.filename)
99 delattr(self, 'filename')
101 # The temporary file will be created in a directory set by the
102 # environment (TEMP_DIR, TEMP or TMP)
103 self.filename, header = urllib.urlretrieve(self.fp_file.name)
104 name = os.path.basename(self.filename)
105 disposition = header.get('Content-Disposition')
107 name = disposition.rpartition("filename=")[2].strip('" ')
108 filename = header.get('X-File-Name')
112 self.tempfile = open(self.filename, 'r')
113 return File(self.tempfile, name=name)
115 def save(self, *args, **kwargs):
116 if self.name and not self.slug:
117 self._generate_unique_slug()
118 super(Document, self).save(*args, **kwargs)
120 class Note(Document):
121 """ A django model representing an uploaded file and associated metadata.
123 # FIXME: refactor file choices after FP.io integration
125 FILE_TYPE_CHOICES = (
126 ('doc', 'MS Word compatible file (.doc, .docx, .rtf, .odf)'),
127 ('img', 'Scan or picture of notes'),
129 ('ppt', 'Powerpoint'),
131 (UNKNOWN_FILE, 'Unknown file'),
134 file_type = models.CharField(max_length=15, \
135 choices=FILE_TYPE_CHOICES, \
136 default=UNKNOWN_FILE, \
137 blank=True, null=True)
139 # Upload files to MEDIA_ROOT/notes/YEAR/MONTH/DAY, 2012/10/30/filename
140 pdf_file = models.FileField( \
142 upload_to="notes/%Y/%m/%d/",\
143 blank=True, null=True)
144 # No longer keeping a local copy backed by django
145 note_file = models.FileField( \
147 upload_to="notes/%Y/%m/%d/",\
148 blank=True, null=True)
151 embed_url = models.URLField(max_length=1024, blank=True, null=True)
152 download_url = models.URLField(max_length=1024, blank=True, null=True)
154 # Generated by Google Drive by saved locally
155 html = models.TextField(blank=True, null=True)
156 text = models.TextField(blank=True, null=True)
159 # not using, but keeping old data
160 year = models.IntegerField(blank=True, null=True,\
161 default=datetime.datetime.utcnow().year)
162 desc = models.TextField(max_length=511, blank=True, null=True)
164 is_flagged = models.BooleanField(default=False)
165 is_moderated = models.BooleanField(default=False)
168 def __unicode__(self):
169 return u"Note: {0} {1} -- {2}".format(self.file_type, self.name, self.uploaded_at)
172 def get_absolute_url(self):
173 """ Resolve note url, use 'note' route and slug if slug
174 otherwise use note.id
176 if self.slug is not None:
177 # return a url ending in slug
178 return u"/{0}/{1}/{2}".format(self.course.school.slug, self.course.slug, self.slug)
180 # return a url ending in id
181 return u"/{0}/{1}/{2}".format(self.course.school.slug, self.course.slug, self.id)
183 def sanitize_html(self, save=True):
184 """ if self contains html, find all <a> tags and add target=_blank
186 returns True/False on succ/fail and error or count
188 # build a tag sanitizer
189 def add_attribute_target(tag):
190 tag.attrib['target'] = '_blank'
192 # if no html, return false
194 return False, "Note has no html"
196 _html = fromstring(self.html)
197 a_tags = _html.findall('.//a') # recursively find all a tags in document tree
198 # if there are a tags
200 #apply the add attribute function
201 map(add_attribute_target, a_tags)
205 return True, len(a_tags)
207 def _update_parent_updated_at(self):
208 """ update the parent Course.updated_at model
209 with the latest uploaded_at """
210 self.course.updated_at = self.uploaded_at
213 def save(self, *args, **kwargs):
214 if self.uploaded_at and self.uploaded_at > self.course.updated_at:
215 self._update_parent_updated_at()
216 super(Note, self).save(*args, **kwargs)
219 class DriveAuth(models.Model):
220 """ stored google drive authentication and refresh token
221 used for interacting with google drive """
223 email = models.EmailField(default=GOOGLE_USER)
224 credentials = models.TextField() # JSON of Oauth2Credential object
225 stored_at = models.DateTimeField(auto_now=True)
229 def get(email=GOOGLE_USER):
230 """ Staticmethod for getting the singleton DriveAuth object """
231 # FIXME: this is untested
232 return DriveAuth.objects.filter(email=email).reverse()[0]
235 def store(self, creds):
236 """ Transform an existing credentials object to a db serialized """
237 self.email = creds.id_token['email']
238 self.credentials = creds.to_json()
242 def transform_to_cred(self):
243 """ take stored credentials and produce a Credentials object """
244 return Credentials.new_from_json(self.credentials)
247 def __unicode__(self):
248 return u'Gdrive auth for %s created/updated at %s' % \
249 (self.email, self.stored_at)