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