cc7a262991ca8e20089dcd55de85e3d3611760a1
[oweals/karmaworld.git] / karmaworld / apps / notes / models.py
1 #!/usr/bin/env python
2 # -*- coding:utf8 -*-
3 # Copyright (C) 2012  FinalsClub Foundation
4
5 """
6     Models for the notes django app.
7     Contains only the minimum for handling files and their representation
8 """
9 import datetime
10 import os
11 import urllib
12
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
22
23 from karmaworld.apps.courses.models import Course
24
25 try:
26     from secrets.drive import GOOGLE_USER
27 except:
28     GOOGLE_USER = u'admin@karmanotes.org'
29
30 fs = FileSystemStorage(location=settings.MEDIA_ROOT)
31
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)
40
41 class Document(models.Model):
42     """ An Abstract Base Class representing a document
43         intended to be subclassed
44
45     """
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)
50
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)
55
56
57     # if True, NEVER show this file
58     # WARNING: This may throw an error on migration
59     is_hidden       = models.BooleanField(default=False)
60
61     fp_file = django_filepicker.models.FPFileField(
62             upload_to=_choose_upload_to,
63             storage=fs,                 \
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)
67
68     class Meta:
69         abstract = True
70         ordering = ['-uploaded_at']
71
72     def __unicode__(self):
73         return u"Document: {1} -- {2}".format(self.name, self.uploaded_at)
74
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)
80         if collision:
81             _slug = u"{0}-{1}-{2}-{3}".format(
82                     _slug, self.uploaded_at.month,
83                     self.uploaded_at.day, self.uploaded_at.microsecond)
84         self.slug = _slug
85
86     def get_file(self):
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'):
91             self.tempfile.close()
92             delattr(self, 'tempfile')
93
94         if hasattr(self, 'filename'):
95             # the file might have been moved in the meantime so
96             # check first
97             if os.path.exists(self.filename):
98                 os.remove(self.filename)
99             delattr(self, 'filename')
100
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')
106         if disposition:
107             name = disposition.rpartition("filename=")[2].strip('" ')
108         filename = header.get('X-File-Name')
109         if filename:
110             name = filename
111
112         self.tempfile = open(self.filename, 'r')
113         return File(self.tempfile, name=name)
114
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)
119
120 class Note(Document):
121     """ A django model representing an uploaded file and associated metadata.
122     """
123     # FIXME: refactor file choices after FP.io integration
124     UNKNOWN_FILE = '???'
125     FILE_TYPE_CHOICES = (
126         ('doc', 'MS Word compatible file (.doc, .docx, .rtf, .odf)'),
127         ('img', 'Scan or picture of notes'),
128         ('pdf', 'PDF file'),
129         ('ppt', 'Powerpoint'),
130         ('txt', 'Text'),
131         (UNKNOWN_FILE, 'Unknown file'),
132     )
133
134     file_type       = models.CharField(max_length=15,  \
135                             choices=FILE_TYPE_CHOICES, \
136                             default=UNKNOWN_FILE,      \
137                             blank=True, null=True)
138
139     # Upload files to MEDIA_ROOT/notes/YEAR/MONTH/DAY, 2012/10/30/filename
140     pdf_file       = models.FileField(                  \
141                             storage=fs,                 \
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(                 \
146                             storage=fs,                 \
147                             upload_to="notes/%Y/%m/%d/",\
148                             blank=True, null=True)
149
150     # Google Drive URLs
151     embed_url       = models.URLField(max_length=1024, blank=True, null=True)
152     download_url    = models.URLField(max_length=1024, blank=True, null=True)
153
154     # Generated by Google Drive by saved locally
155     html            = models.TextField(blank=True, null=True)
156     text            = models.TextField(blank=True, null=True)
157
158
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)
163
164     is_flagged      = models.BooleanField(default=False)
165     is_moderated    = models.BooleanField(default=False)
166
167
168     def __unicode__(self):
169         return u"Note: {0} {1} -- {2}".format(self.file_type, self.name, self.uploaded_at)
170
171
172     def get_absolute_url(self):
173         """ Resolve note url, use 'note' route and slug if slug
174             otherwise use note.id
175         """
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)
179         else:
180             # return a url ending in id
181             return u"/{0}/{1}/{2}".format(self.course.school.slug, self.course.slug, self.id)
182
183     def sanitize_html(self, save=True):
184         """ if self contains html, find all <a> tags and add target=_blank
185             takes self
186             returns True/False on succ/fail and error or count
187         """
188         # build a tag sanitizer
189         def add_attribute_target(tag):
190             tag.attrib['target'] = '_blank'
191
192         # if no html, return false
193         if not self.html:
194             return False, "Note has no html"
195
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
199         if a_tags > 1:
200             #apply the add attribute function
201             map(add_attribute_target, a_tags)
202             self.html = _html
203             if save:
204                 self.save()
205             return True, len(a_tags)
206
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
211         self.course.save()
212
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)
217
218
219 class DriveAuth(models.Model):
220     """ stored google drive authentication and refresh token
221         used for interacting with google drive """
222
223     email = models.EmailField(default=GOOGLE_USER)
224     credentials = models.TextField() # JSON of Oauth2Credential object
225     stored_at = models.DateTimeField(auto_now=True)
226
227
228     @staticmethod
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]
233
234
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()
239         self.save()
240
241
242     def transform_to_cred(self):
243         """ take stored credentials and produce a Credentials object """
244         return Credentials.new_from_json(self.credentials)
245
246
247     def __unicode__(self):
248         return u'Gdrive auth for %s created/updated at %s' % \
249                     (self.email, self.stored_at)