Test-Remove slug duplicate look-up for the moment
[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         collision = True
81         if collision:
82             _slug = u"{0}-{1}-{2}-{3}".format(
83                     _slug, self.uploaded_at.month,
84                     self.uploaded_at.day, self.uploaded_at.microsecond)
85         self.slug = _slug
86
87     def get_file(self):
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'):
92             self.tempfile.close()
93             delattr(self, 'tempfile')
94
95         if hasattr(self, 'filename'):
96             # the file might have been moved in the meantime so
97             # check first
98             if os.path.exists(self.filename):
99                 os.remove(self.filename)
100             delattr(self, 'filename')
101
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')
107         if disposition:
108             name = disposition.rpartition("filename=")[2].strip('" ')
109         filename = header.get('X-File-Name')
110         if filename:
111             name = filename
112
113         self.tempfile = open(self.filename, 'r')
114         return File(self.tempfile, name=name)
115
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)
120
121 class Note(Document):
122     """ A django model representing an uploaded file and associated metadata.
123     """
124     # FIXME: refactor file choices after FP.io integration
125     UNKNOWN_FILE = '???'
126     FILE_TYPE_CHOICES = (
127         ('doc', 'MS Word compatible file (.doc, .docx, .rtf, .odf)'),
128         ('img', 'Scan or picture of notes'),
129         ('pdf', 'PDF file'),
130         ('ppt', 'Powerpoint'),
131         ('txt', 'Text'),
132         (UNKNOWN_FILE, 'Unknown file'),
133     )
134
135     file_type       = models.CharField(max_length=15,  \
136                             choices=FILE_TYPE_CHOICES, \
137                             default=UNKNOWN_FILE,      \
138                             blank=True, null=True)
139
140     # Upload files to MEDIA_ROOT/notes/YEAR/MONTH/DAY, 2012/10/30/filename
141     pdf_file       = models.FileField(                  \
142                             storage=fs,                 \
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(                 \
147                             storage=fs,                 \
148                             upload_to="notes/%Y/%m/%d/",\
149                             blank=True, null=True)
150
151     # Google Drive URLs
152     embed_url       = models.URLField(max_length=1024, blank=True, null=True)
153     download_url    = models.URLField(max_length=1024, blank=True, null=True)
154
155     # Generated by Google Drive by saved locally
156     html            = models.TextField(blank=True, null=True)
157     text            = models.TextField(blank=True, null=True)
158
159
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)
164
165     is_flagged      = models.BooleanField(default=False)
166     is_moderated    = models.BooleanField(default=False)
167
168
169     def __unicode__(self):
170         return u"Note: {0} {1} -- {2}".format(self.file_type, self.name, self.uploaded_at)
171
172
173     def get_absolute_url(self):
174         """ Resolve note url, use 'note' route and slug if slug
175             otherwise use note.id
176         """
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)
180         else:
181             # return a url ending in id
182             return u"/{0}/{1}/{2}".format(self.course.school.slug, self.course.slug, self.id)
183
184     def sanitize_html(self, save=True):
185         """ if self contains html, find all <a> tags and add target=_blank
186             takes self
187             returns True/False on succ/fail and error or count
188         """
189         # build a tag sanitizer
190         def add_attribute_target(tag):
191             tag.attrib['target'] = '_blank'
192
193         # if no html, return false
194         if not self.html:
195             return False, "Note has no html"
196
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
200         if a_tags > 1:
201             #apply the add attribute function
202             map(add_attribute_target, a_tags)
203             self.html = _html
204             if save:
205                 self.save()
206             return True, len(a_tags)
207
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
212         self.course.save()
213
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)
218
219
220 class DriveAuth(models.Model):
221     """ stored google drive authentication and refresh token
222         used for interacting with google drive """
223
224     email = models.EmailField(default=GOOGLE_USER)
225     credentials = models.TextField() # JSON of Oauth2Credential object
226     stored_at = models.DateTimeField(auto_now=True)
227
228
229     @staticmethod
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]
234
235
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()
240         self.save()
241
242
243     def transform_to_cred(self):
244         """ take stored credentials and produce a Credentials object """
245         return Credentials.new_from_json(self.credentials)
246
247
248     def __unicode__(self):
249         return u'Gdrive auth for %s created/updated at %s' % \
250                     (self.email, self.stored_at)