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)
82 _slug = u"{0}-{1}-{2}-{3}".format(
83 _slug, self.uploaded_at.month,
84 self.uploaded_at.day, self.uploaded_at.microsecond)
88 """ Downloads the file from filepicker.io and returns a
89 Django File wrapper object """
90 # clean up any old downloads that are still hanging around
91 if hasattr(self, 'tempfile'):
93 delattr(self, 'tempfile')
95 if hasattr(self, 'filename'):
96 # the file might have been moved in the meantime so
98 if os.path.exists(self.filename):
99 os.remove(self.filename)
100 delattr(self, 'filename')
102 # The temporary file will be created in a directory set by the
103 # environment (TEMP_DIR, TEMP or TMP)
104 self.filename, header = urllib.urlretrieve(self.fp_file.name)
105 name = os.path.basename(self.filename)
106 disposition = header.get('Content-Disposition')
108 name = disposition.rpartition("filename=")[2].strip('" ')
109 filename = header.get('X-File-Name')
113 self.tempfile = open(self.filename, 'r')
114 return File(self.tempfile, name=name)
116 def save(self, *args, **kwargs):
117 if self.name and not self.slug:
118 self._generate_unique_slug()
119 super(Document, self).save(*args, **kwargs)
121 class Note(Document):
122 """ A django model representing an uploaded file and associated metadata.
124 # FIXME: refactor file choices after FP.io integration
126 FILE_TYPE_CHOICES = (
127 ('doc', 'MS Word compatible file (.doc, .docx, .rtf, .odf)'),
128 ('img', 'Scan or picture of notes'),
130 ('ppt', 'Powerpoint'),
132 (UNKNOWN_FILE, 'Unknown file'),
135 file_type = models.CharField(max_length=15, \
136 choices=FILE_TYPE_CHOICES, \
137 default=UNKNOWN_FILE, \
138 blank=True, null=True)
140 # Upload files to MEDIA_ROOT/notes/YEAR/MONTH/DAY, 2012/10/30/filename
141 pdf_file = models.FileField( \
143 upload_to="notes/%Y/%m/%d/",\
144 blank=True, null=True)
145 # No longer keeping a local copy backed by django
146 note_file = models.FileField( \
148 upload_to="notes/%Y/%m/%d/",\
149 blank=True, null=True)
152 embed_url = models.URLField(max_length=1024, blank=True, null=True)
153 download_url = models.URLField(max_length=1024, blank=True, null=True)
155 # Generated by Google Drive by saved locally
156 html = models.TextField(blank=True, null=True)
157 text = models.TextField(blank=True, null=True)
160 # not using, but keeping old data
161 year = models.IntegerField(blank=True, null=True,\
162 default=datetime.datetime.utcnow().year)
163 desc = models.TextField(max_length=511, blank=True, null=True)
165 is_flagged = models.BooleanField(default=False)
166 is_moderated = models.BooleanField(default=False)
169 def __unicode__(self):
170 return u"Note: {0} {1} -- {2}".format(self.file_type, self.name, self.uploaded_at)
173 def get_absolute_url(self):
174 """ Resolve note url, use 'note' route and slug if slug
175 otherwise use note.id
177 if self.slug is not None:
178 # return a url ending in slug
179 return u"/{0}/{1}/{2}".format(self.course.school.slug, self.course.slug, self.slug)
181 # return a url ending in id
182 return u"/{0}/{1}/{2}".format(self.course.school.slug, self.course.slug, self.id)
184 def sanitize_html(self, save=True):
185 """ if self contains html, find all <a> tags and add target=_blank
187 returns True/False on succ/fail and error or count
189 # build a tag sanitizer
190 def add_attribute_target(tag):
191 tag.attrib['target'] = '_blank'
193 # if no html, return false
195 return False, "Note has no html"
197 _html = fromstring(self.html)
198 a_tags = _html.findall('.//a') # recursively find all a tags in document tree
199 # if there are a tags
201 #apply the add attribute function
202 map(add_attribute_target, a_tags)
206 return True, len(a_tags)
208 def _update_parent_updated_at(self):
209 """ update the parent Course.updated_at model
210 with the latest uploaded_at """
211 self.course.updated_at = self.uploaded_at
214 def save(self, *args, **kwargs):
215 if self.uploaded_at and self.uploaded_at > self.course.updated_at:
216 self._update_parent_updated_at()
217 super(Note, self).save(*args, **kwargs)
220 class DriveAuth(models.Model):
221 """ stored google drive authentication and refresh token
222 used for interacting with google drive """
224 email = models.EmailField(default=GOOGLE_USER)
225 credentials = models.TextField() # JSON of Oauth2Credential object
226 stored_at = models.DateTimeField(auto_now=True)
230 def get(email=GOOGLE_USER):
231 """ Staticmethod for getting the singleton DriveAuth object """
232 # FIXME: this is untested
233 return DriveAuth.objects.filter(email=email).reverse()[0]
236 def store(self, creds):
237 """ Transform an existing credentials object to a db serialized """
238 self.email = creds.id_token['email']
239 self.credentials = creds.to_json()
243 def transform_to_cred(self):
244 """ take stored credentials and produce a Credentials object """
245 return Credentials.new_from_json(self.credentials)
248 def __unicode__(self):
249 return u'Gdrive auth for %s created/updated at %s' % \
250 (self.email, self.stored_at)