Update note counts when a note is create or deleted
[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 from django.db.models.signals import post_save, post_delete
11 from django.dispatch import receiver
12 import os
13 import urllib
14
15 from django.conf import settings
16 from django.core.files import File
17 from django.core.files.storage import FileSystemStorage
18 from django.db import models
19 from django.template import defaultfilters
20 import django_filepicker
21 from lxml.html import fromstring, tostring
22 from oauth2client.client import Credentials
23 from taggit.managers import TaggableManager
24
25 from karmaworld.apps.courses.models import Course
26 from karmaworld.apps.users.models import KarmaUser
27
28 try:
29     from secrets.drive import GOOGLE_USER
30 except:
31     GOOGLE_USER = u'admin@karmanotes.org'
32
33 fs = FileSystemStorage(location=settings.MEDIA_ROOT)
34
35 def _choose_upload_to(instance, filename):
36     # /school/course/year/month/day
37     return u"{school}/{course}/{year}/{month}/{day}".format(
38         school=instance.course.school.slug,
39         course=instance.course.slug,
40         year=instance.uploaded_at.year,
41         month=instance.uploaded_at.month,
42         day=instance.uploaded_at.day)
43
44 class Document(models.Model):
45     """ An Abstract Base Class representing a document
46         intended to be subclassed
47
48     """
49     course          = models.ForeignKey(Course)
50     tags            = TaggableManager(blank=True)
51     name            = models.CharField(max_length=255, blank=True, null=True)
52     slug            = models.SlugField(max_length=255, null=True)
53
54     # metadata relevant to the Upload process
55     user            = models.ForeignKey('users.KarmaUser', null=True)
56     ip              = models.IPAddressField(blank=True, null=True,
57                         help_text=u"IP address of the uploader")
58     uploaded_at     = models.DateTimeField(null=True, default=datetime.datetime.utcnow)
59
60
61     # if True, NEVER show this file
62     # WARNING: This may throw an error on migration
63     is_hidden       = models.BooleanField(default=False)
64
65     fp_file = django_filepicker.models.FPFileField(
66             upload_to=_choose_upload_to,
67             storage=fs,
68             null=True, blank=True,
69             help_text=u"An uploaded file reference from Filepicker.io")
70     mimetype = models.CharField(max_length=255, blank=True, null=True)
71
72     class Meta:
73         abstract = True
74         ordering = ['-uploaded_at']
75
76     def __unicode__(self):
77         return u"Document: {1} -- {2}".format(self.name, self.uploaded_at)
78
79     def _generate_unique_slug(self):
80         """ generate a unique slug based on name and uploaded_at  """
81         _slug = defaultfilters.slugify(self.name)
82         klass = self.__class__
83         collision = klass.objects.filter(slug=_slug)
84         if collision:
85             _slug = u"{0}-{1}-{2}-{3}".format(
86                     _slug, self.uploaded_at.month,
87                     self.uploaded_at.day, self.uploaded_at.microsecond)
88         self.slug = _slug
89
90     def get_file(self):
91         """ Downloads the file from filepicker.io and returns a
92         Django File wrapper object """
93         # clean up any old downloads that are still hanging around
94         if hasattr(self, 'tempfile'):
95             self.tempfile.close()
96             delattr(self, 'tempfile')
97
98         if hasattr(self, 'filename'):
99             # the file might have been moved in the meantime so
100             # check first
101             if os.path.exists(self.filename):
102                 os.remove(self.filename)
103             delattr(self, 'filename')
104
105         # The temporary file will be created in a directory set by the
106         # environment (TEMP_DIR, TEMP or TMP)
107         self.filename, header = urllib.urlretrieve(self.fp_file.name)
108         name = os.path.basename(self.filename)
109         disposition = header.get('Content-Disposition')
110         if disposition:
111             name = disposition.rpartition("filename=")[2].strip('" ')
112         filename = header.get('X-File-Name')
113         if filename:
114             name = filename
115
116         self.tempfile = open(self.filename, 'r')
117         return File(self.tempfile, name=name)
118
119     def save(self, *args, **kwargs):
120         if self.name and not self.slug:
121             self._generate_unique_slug()
122         super(Document, self).save(*args, **kwargs)
123
124 class Note(Document):
125     """ A django model representing an uploaded file and associated metadata.
126     """
127     # FIXME: refactor file choices after FP.io integration
128     UNKNOWN_FILE = '???'
129     FILE_TYPE_CHOICES = (
130         ('doc', 'MS Word compatible file (.doc, .docx, .rtf, .odf)'),
131         ('img', 'Scan or picture of notes'),
132         ('pdf', 'PDF file'),
133         ('ppt', 'Powerpoint'),
134         ('txt', 'Text'),
135         (UNKNOWN_FILE, 'Unknown file'),
136     )
137
138     file_type       = models.CharField(max_length=15,  \
139                             choices=FILE_TYPE_CHOICES, \
140                             default=UNKNOWN_FILE,      \
141                             blank=True, null=True)
142
143     # Upload files to MEDIA_ROOT/notes/YEAR/MONTH/DAY, 2012/10/30/filename
144     pdf_file       = models.FileField(                  \
145                             storage=fs,                 \
146                             upload_to="notes/%Y/%m/%d/",\
147                             blank=True, null=True)
148     # No longer keeping a local copy backed by django
149     note_file       = models.FileField(                 \
150                             storage=fs,                 \
151                             upload_to="notes/%Y/%m/%d/",\
152                             blank=True, null=True)
153
154     # Google Drive URLs
155     embed_url       = models.URLField(max_length=1024, blank=True, null=True)
156     download_url    = models.URLField(max_length=1024, blank=True, null=True)
157
158     # Generated by Google Drive by saved locally
159     html            = models.TextField(blank=True, null=True)
160     text            = models.TextField(blank=True, null=True)
161
162
163     # not using, but keeping old data
164     year            = models.IntegerField(blank=True, null=True,\
165                         default=datetime.datetime.utcnow().year)
166     desc            = models.TextField(max_length=511, blank=True, null=True)
167
168     is_flagged      = models.BooleanField(default=False)
169     is_moderated    = models.BooleanField(default=False)
170
171
172     def __unicode__(self):
173         return u"Note: {0} {1} -- {2}".format(self.file_type, self.name, self.uploaded_at)
174
175
176     def get_absolute_url(self):
177         """ Resolve note url, use 'note' route and slug if slug
178             otherwise use note.id
179         """
180         if self.slug is not None:
181             # return a url ending in slug
182             return u"/{0}/{1}/{2}".format(self.course.school.slug, self.course.slug, self.slug)
183         else:
184             # return a url ending in id
185             return u"/{0}/{1}/{2}".format(self.course.school.slug, self.course.slug, self.id)
186
187     def sanitize_html(self, save=True):
188         """ if self contains html, find all <a> tags and add target=_blank
189             takes self
190             returns True/False on succ/fail and error or count
191         """
192         # build a tag sanitizer
193         def add_attribute_target(tag):
194             tag.attrib['target'] = '_blank'
195
196         # if no html, return false
197         if not self.html:
198             return False, "Note has no html"
199
200         _html = fromstring(self.html)
201         a_tags = _html.findall('.//a') # recursively find all a tags in document tree
202         # if there are a tags
203         if a_tags > 1:
204             #apply the add attribute function
205             map(add_attribute_target, a_tags)
206             self.html = tostring(_html)
207             if save:
208                 self.save()
209             return True, len(a_tags)
210
211     def _update_parent_updated_at(self):
212         """ update the parent Course.updated_at model
213             with the latest uploaded_at """
214         self.course.updated_at = self.uploaded_at
215         self.course.save()
216
217     def save(self, *args, **kwargs):
218         if self.uploaded_at and self.uploaded_at > self.course.updated_at:
219             self._update_parent_updated_at()
220         super(Note, self).save(*args, **kwargs)
221
222
223 def update_note_counts(note_instance):
224     note_instance.course.update_note_count()
225     note_instance.course.school.update_note_count()
226
227 @receiver(post_save, sender=Note, weak=False)
228 def note_receiver(sender, **kwargs):
229     if kwargs['created']:
230         update_note_counts(kwargs['instance'])
231
232 @receiver(post_delete, sender=Note, weak=False)
233 def note_receiver(sender, **kwargs):
234     update_note_counts(kwargs['instance'])
235
236
237 class DriveAuth(models.Model):
238     """ stored google drive authentication and refresh token
239         used for interacting with google drive """
240
241     email = models.EmailField(default=GOOGLE_USER)
242     credentials = models.TextField() # JSON of Oauth2Credential object
243     stored_at = models.DateTimeField(auto_now=True)
244
245
246     @staticmethod
247     def get(email=GOOGLE_USER):
248         """ Staticmethod for getting the singleton DriveAuth object """
249         # FIXME: this is untested
250         return DriveAuth.objects.filter(email=email).reverse()[0]
251
252
253     def store(self, creds):
254         """ Transform an existing credentials object to a db serialized """
255         self.email = creds.id_token['email']
256         self.credentials = creds.to_json()
257         self.save()
258
259
260     def transform_to_cred(self):
261         """ take stored credentials and produce a Credentials object """
262         return Credentials.new_from_json(self.credentials)
263
264
265     def __unicode__(self):
266         return u'Gdrive auth for %s created/updated at %s' % \
267                     (self.email, self.stored_at)