change file upload directory structure
[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
11 from django.conf import settings
12 from django.core.files.storage import FileSystemStorage
13 from django.db import models
14 from django.template import defaultfilters
15 import django_filepicker
16 from lxml.html import fromstring, tostring
17 from oauth2client.client import Credentials
18 from taggit.managers import TaggableManager
19
20 from karmaworld.apps.courses.models import Course
21
22 try:
23     from secrets.drive import GOOGLE_USER
24 except:
25     GOOGLE_USER = u'admin@karmanotes.org'
26
27 fs = FileSystemStorage(location=settings.MEDIA_ROOT)
28
29 def _choose_upload_to(instance, filename):
30     # /school/course/year/month/day
31     return u"{school}/{course}/{year}/{month}/{day}".format(
32         school=instance.course.school.slug,
33         course=instance.course.slug,
34         year=instance.uploaded_at.year,
35         month=instance.uploaded_at.month,
36         day=instance.uploaded_at.day)
37
38 class Document(models.Model):
39     """ An Abstract Base Class representing a document
40         intended to be subclassed
41
42     """
43     course          = models.ForeignKey(Course)
44     tags            = TaggableManager(blank=True)
45     name            = models.CharField(max_length=255, blank=True, null=True)
46     slug            = models.SlugField(max_length=255, null=True)
47
48     # metadata relevant to the Upload process
49     ip      = models.IPAddressField(blank=True, null=True,
50                 help_text=u"IP address of the uploader")
51     uploaded_at     = models.DateTimeField(null=True, default=datetime.datetime.utcnow)
52
53
54     # if True, NEVER show this file
55     # WARNING: This may throw an error on migration
56     is_hidden       = models.BooleanField(default=False)
57
58     fp_file = django_filepicker.models.FPFileField(
59             upload_to=_choose_upload_to,
60             null=True, blank=True,
61             help_text=u"An uploaded file reference from Filepicker.io")
62
63     class Meta:
64         abstract = True
65         ordering = ['-uploaded_at']
66
67     def __unicode__(self):
68         return u"Document: {1} -- {2}".format(self.name, self.uploaded_at)
69
70     def _generate_unique_slug(self):
71         """ generate a unique slug based on name and uploaded_at  """
72         _slug = defaultfilters.slugify(self.name)
73         klass = self.__class__
74         collision = klass.objects.filter(slug=self.slug)
75         if collision:
76             _slug = u"{0}-{1}-{2}-{3}".format(
77                     _slug, self.uploaded_at.month,
78                     self.uploaded_at.day, self.uploaded_at.microsecond)
79         self.slug = _slug
80
81     def save(self, *args, **kwargs):
82         if self.name and not self.slug:
83             self._generate_unique_slug()
84         super(Document, self).save(*args, **kwargs)
85
86 class Note(Document):
87     """ A django model representing an uploaded file and associated metadata.
88     """
89     # FIXME: refactor file choices after FP.io integration
90     UNKNOWN_FILE = '???'
91     FILE_TYPE_CHOICES = (
92         ('doc', 'MS Word compatible file (.doc, .docx, .rtf, .odf)'),
93         ('img', 'Scan or picture of notes'),
94         ('pdf', 'PDF file'),
95         ('ppt', 'Powerpoint'),
96         (UNKNOWN_FILE, 'Unknown file'),
97     )
98
99     file_type       = models.CharField(max_length=15,  \
100                             choices=FILE_TYPE_CHOICES, \
101                             default=UNKNOWN_FILE,      \
102                             blank=True, null=True)
103
104     # Upload files to MEDIA_ROOT/notes/YEAR/MONTH/DAY, 2012/10/30/filename
105     # FIXME: because we are adding files by hand in tasks.py, upload_to is being ignored for media files
106     pdf_file       = models.FileField(                  \
107                             storage=fs,                 \
108                             upload_to="notes/%Y/%m/%j/",\
109                             blank=True, null=True)
110     # No longer keeping a local copy backed by django
111     note_file       = models.FileField(                 \
112                             storage=fs,                 \
113                             upload_to="notes/%Y/%m/%j/",\
114                             blank=True, null=True)
115
116     # Google Drive URLs
117     embed_url       = models.URLField(max_length=1024, blank=True, null=True)
118     download_url    = models.URLField(max_length=1024, blank=True, null=True)
119
120     # Generated by Google Drive by saved locally
121     html            = models.TextField(blank=True, null=True)
122     text            = models.TextField(blank=True, null=True)
123
124
125     # not using, but keeping old data
126     year            = models.IntegerField(blank=True, null=True,\
127                         default=datetime.datetime.utcnow().year)
128     desc            = models.TextField(max_length=511, blank=True, null=True)
129
130     is_flagged      = models.BooleanField(default=False)
131     is_moderated    = models.BooleanField(default=False)
132
133
134     def __unicode__(self):
135         return u"Note: {0} {1} -- {2}".format(self.file_type, self.name, self.uploaded_at)
136
137
138     def get_absolute_url(self):
139         """ Resolve note url, use 'note' route and slug if slug
140             otherwise use note.id
141         """
142         if self.slug is not None:
143             # return a url ending in slug
144             return u"/{0}/{1}/{2}".format(self.course.school.slug, self.course.slug, self.slug)
145         else:
146             # return a url ending in id
147             return u"/{0}/{1}/{2}".format(self.course.school.slug, self.course.slug, self.id)
148
149     def sanitize_html(self, save=True):
150         """ if self contains html, find all <a> tags and add target=_blank
151             takes self
152             returns True/False on succ/fail and error or count
153         """
154         # build a tag sanitizer
155         def add_attribute_target(tag):
156             tag.attrib['target'] = '_blank'
157
158         # if no html, return false
159         if not self.html:
160             return False, "Note has no html"
161
162         _html = fromstring(self.html)
163         a_tags = _html.findall('.//a') # recursively find all a tags in document tree
164         # if there are a tags
165         if a_tags > 1:
166             #apply the add attribute function
167             map(add_attribute_target, a_tags)
168             self.html = _html
169             if save:
170                 self.save()
171             return True, len(a_tags)
172
173     def _update_parent_updated_at(self):
174         """ update the parent Course.updated_at model
175             with the latest uploaded_at """
176         self.course.updated_at = self.uploaded_at
177         self.course.save()
178
179     def save(self, *args, **kwargs):
180         if self.uploaded_at and self.uploaded_at > self.course.updated_at:
181             self._update_parent_updated_at()
182         super(Note, self).save(*args, **kwargs)
183
184
185 class DriveAuth(models.Model):
186     """ stored google drive authentication and refresh token
187         used for interacting with google drive """
188
189     email = models.EmailField(default=GOOGLE_USER)
190     credentials = models.TextField() # JSON of Oauth2Credential object
191     stored_at = models.DateTimeField(auto_now=True)
192
193
194     @staticmethod
195     def get(email=GOOGLE_USER):
196         """ Staticmethod for getting the singleton DriveAuth object """
197         # FIXME: this is untested
198         return DriveAuth.objects.filter(email=email).reverse()[0]
199
200
201     def store(self, creds):
202         """ Transform an existing credentials object to a db serialized """
203         self.email = creds.id_token['email']
204         self.credentials = creds.to_json()
205         self.save()
206
207
208     def transform_to_cred(self):
209         """ take stored credentials and produce a Credentials object """
210         return Credentials.new_from_json(self.credentials)
211
212
213     def __unicode__(self):
214         return u'Gdrive auth for %s created/updated at %s' % \
215                     (self.email, self.stored_at)