from django.db import models from files.models import File, Category from fields import HexColorField from django.contrib.comments.signals import comment_was_posted import simplejson from django.core.mail import send_mail import os from os.path import join from PIL import Image from django.template import Template, Context from django.template.loader import get_template from settings import MEDIA_ROOT, PROJECT_PATH, SITE_BASE from django.contrib.auth.models import User, Group from tagging.fields import TagField from tagging.models import Tag from copy import deepcopy from sorl.thumbnail import get_thumbnail from django.db.models import Q def addPx(val): ''' Adds 'px' to an integer value for a CSS property from DB ''' r = str(int(round(val))) + "px" return r def cleanCSS(prop): ''' Takes a CSS property and returns it into a value safe for insertion into DB ''' propStr = str(prop) if propStr[-2:] == 'px': r = int(propStr[:-2]) # elif prop[0] == '#': # r = prop[1:] else: r = prop return r def isPx(s): if s[:-2] == 'px': return True else: return False def baseFileName(filename): r = filename.rindex('.') return filename[0:r] def extFileName(filename): r = filename.rindex('.') + 1 return filename[r:] class LinkCategory(models.Model): title = models.CharField(max_length=255) is_onfront = models.BooleanField(default=False) class Link(models.Model): product = models.ForeignKey("Product") category = models.ForeignKey("LinkCategory") thumbnail = models.ImageField(upload_to='images/link_thumbs/') description = models.TextField(blank=True, null=True) order = models.IntegerField() def __unicode__(self): return self.description class ProductType(models.Model): name = models.CharField(max_length=255) aspect_ratio = models.FloatField(default=0.707, help_text="Default 0.707 as per http://en.wikipedia.org/wiki/Paper_size") print_width = models.IntegerField(default=210, help_text="Default: A4 (210mm) Unit: mm") def __unicode__(self): return self.name class Product(models.Model): title = models.CharField(max_length=255) published = models.BooleanField(default=False) date_published = models.DateField(blank=True, null=True) typ = models.ForeignKey("ProductType") tags = TagField() creator = models.ForeignKey(User) abstract = models.TextField(blank=True, null=True) videos = models.ManyToManyField("Video", blank=True) audios = models.ManyToManyField("Audio", blank=True) def __unicode__(self): return "%d: %s" % (self.id, self.title,) def get_print_multiplier(self, dpi): # product = Product.objects.get(pk=self.product.id) print_width_mm = self.typ.print_width dpm = dpi / 25.4 pixel_width = print_width_mm * dpm m = pixel_width / 800.0 return m def get_page_list(self): pages = [] articles = Article.objects.filter(page=self).order_by('order') for a in articles: a_pages = Page.objects.filter(article=a).order_by('page_no') pages.extend(a_pages) return pages def get_view_size(self, width=800): aspect_ratio = self.typ.aspect_ratio width = width + .0 height = int(round(width / aspect_ratio)) return (int(round(width)), height,) class Video(models.Model): fil = models.ForeignKey(File) srt = models.ManyToManyField("Srt", blank=True, null=True) def __unicode__(self): return self.fil.title class Audio(models.Model): fil = models.ForeignKey(File) srt = models.ManyToManyField("Srt", blank=True, null=True) def __unicode__(self): return self.fil.title SRT_LANGS = ( ('en', 'English'), ('ar', 'Arabic'), ) SRT_TYPES = ( ('transcript', 'Transcript'), ('description', 'Description'), ) class Srt(models.Model): fil = models.FileField(upload_to="srt/") lang = models.CharField(max_length=20, choices=SRT_LANGS) typ = models.CharField(max_length = 40, choices=SRT_TYPES) ''' This models holds revisions, gets saved to whenever anything in an article changes - when a box changes, its pretty straightforward what's supposed to happen. When a new box or page is created, slightly strange things happen: For a new box- prop = 'new_box', old_val='', new_val=String JSON representation of the new box. For new page- prop = 'new_page', old_val='', new_val=String JSON representation of the new page. For delete box, prop = 'delete_box' For image_crop, prop = 'crop', For image_resize, prop = 'image_resize' (of course, this is ugly). ''' class Revision(models.Model): article = models.ForeignKey("Article") page = models.ForeignKey("Page") user = models.ForeignKey(User) box_type = models.CharField(max_length=100) box_id = models.IntegerField() prop = models.CharField(max_length=100) old_val = models.TextField() new_val = models.TextField() uuid = models.IntegerField() def saveRevision(r): page = Page.objects.get(pk=r['page_id']) article = page.article #FIXME: all calls to saveRevision should always send user, so this should not be required. if r.has_key('user'): user = r['user'] else: user = User.objects.get(pk=1) rev = Revision(article=article, page=page, user=user, box_type=r['box_type'], box_id=r['box_id'], prop=r['prop'], old_val=r['old_val'], new_val=r['new_val'], uuid=r['uuid']) rev.save() return rev.id class ArticleTheme(models.Model): name = models.CharField(max_length=255) description = models.TextField(blank=True, null=True) def __unicode__(self): return self.name def get_dict(self): return { 'id': self.id, 'name': self.name } class Article(models.Model): ''' Each page references an article. A single page cannot reference more than one article. The article is what people comment on (and potentially what audio & video are attached to). ''' name = models.CharField(max_length=255) description = models.TextField(blank=True, null=True) product = models.ForeignKey("Product", blank=True, null=True) typ = models.ForeignKey("ProductType") order = models.IntegerField() #Not needed, do not use study = models.ForeignKey(Category, blank=True, null=True) tags = TagField(blank=True, null=True) owner = models.ForeignKey(User, null=True, related_name='article_owner') created = models.DateTimeField(auto_now_add=True, null=True) locked = models.BooleanField(default=False) published = models.BooleanField(default=False) users = models.ManyToManyField(User, related_name='article_user', blank=True) groups = models.ManyToManyField(Group, blank=True) theme = models.ForeignKey(ArticleTheme, blank=True, null=True) class Meta: unique_together = ('product', 'order',) ordering = ['-created'] ''' Return boolean for whether user can edit article on tool or not - must be passed a valid User object. ''' def can_edit(self, user): if user.is_anonymous(): return False if self.locked: return False if user.is_superuser: return True if self.owner == user: return True for u in self.users.iterator(): if u == User: return True for g in self.groups.iterator(): for u in g.users.iterator(): if u == user: return True return False def is_owner(self, user): if self.owner == user or user.is_superuser: return True else: return False def can_view(self, user): if self.published == True or self.owner == user or user.is_superuser or user in self.users.iterator(): return True for g in self.groups.iterator(): for u in g.users.iterator(): if u == user: return True return False @classmethod def get_can_edit_list(kls, user, qset=False): if not qset: qset = kls.objects.all() return qset.filter(Q(owner=user) | Q(users=user)) # for q in qset: @classmethod def get_published_list(kls, user, qset=False): if not qset: qset = kls.objects.all() return qset.filter(published=True).exclude(Q(owner=user) | Q(users=user)) @classmethod def fts(kls, search, qset=False): if not qset: qset = kls.objects.all() return qset.filter(name__icontains=search) def get_copy(self): a = Article() a.typ = self.typ a.order = 0 a.name = "From Template " + self.name a.save() for p in Page.objects.filter(article=self): new_page = p.get_copy(a) return a ''' Return all changes since revision_no. ''' def changes(self, revision_no, uuid): if int(revision_no) == self.current_revision(): return {'ok': 'ok'} else: d = [] new_revisions_all = Revision.objects.filter(article=self).filter(id__gt=revision_no) new_revisions_others = new_revisions_all.exclude(uuid=uuid) for rev in new_revisions_others: #If there are multiple changes on the same property of the same box, send back only the latest property value. if new_revisions_all.filter(id__gt=rev.id, prop=rev.prop, box_type=rev.box_type, page=rev.page).count() > 0: UGLY_HACK = True else: d.append({ 'page_id': rev.page.id, 'prop': rev.prop, 'old_val': rev.old_val, 'new_val': rev.new_val, 'box_type': rev.box_type, 'box_id': rev.box_id, 'username': rev.user.username, 'uuid': rev.uuid, 'rev_no': rev.id }) last_rev = self.current_revision() return {'revs': d, 'rev_id': last_rev} def current_revision(self): try: rev = Revision.objects.filter(article=self).order_by('-id')[0] return rev.id except: return 0 def editor_size(self): height = self.editor_width / self.aspect_ratio return (self.editor_width, height,) def view_size(self): # product = Product.objects.get(pk=self.product.id) aspect_ratio = self.typ.aspect_ratio width = 800 height = 800.0 / aspect_ratio return (width, height,) def print_size(self, print_width): aspect_ratio = self.typ.aspect_ratio height = int(round(print_width / aspect_ratio)) multiplier = print_width / (self.editor_width + .0) return (self.print_width, height, multiplier,) def get_print_multiplier(self, dpi): # product = Product.objects.get(pk=self.product.id) print_width_mm = self.typ.print_width dpm = dpi / 25.4 pixel_width = print_width_mm * dpm m = pixel_width / 800.0 return m def __unicode__(self): return "%d: %s" % (self.id, self.name,) def get_dict(self, *args): if len(args) > 0: m = args[0] else: m = 1 if len(args) > 1: page_id = args[1] pages = Page.objects.filter(id__iexact=page_id) else: pages = Page.objects.filter(article=self).order_by('page_no') rList = [] for p in pages: rList.append(p.get_dict(m)) return rList def get_list_dict(self, user): return { 'id': self.id, 'title': self.name, 'can_edit': self.can_edit(user), 'edit_url': "/edit/article/%d/" % (self.id,), 'is_locked': self.locked, 'is_published': self.published, 'web_url': SITE_BASE + "/edit/article_web/%d/" % (self.id,) } class PermissionRequest(models.Model): article = models.ForeignKey(Article) from_user = models.ForeignKey(User, related_name='permission_from') message = models.TextField(blank=True, null=True) # to_user = models.ForeignKey(User, related_name='permission_to') accepted = models.BooleanField(default=None) def accept(self): self.accepted = True self.article.users.add(self.from_user) return True def decline(self): self.accepted = False return True @classmethod def new(cls, article, from_user): pr = cls(article=article, from_user=from_user) pr.save() return pr @classmethod def get_pending(cls, article): ret = [] pending_qset = cls.objects.filter(accepted=None) for p in pending_qset: ret.append({ 'user': p.from_user.username, 'message': p.message }) return ret class Page(models.Model): # Question: Does Page need some custom CSS definitions like bg_color, borders, etc. ? page_no = models.IntegerField() article = models.ForeignKey('Article') videos = models.ManyToManyField(Video) audios = models.ManyToManyField(Audio) background_color = models.CharField(max_length=30) def __unicode__(self): return "%s: %s" % (self.page_no, self.article) def set_page_no(self, new_page_no): """ Use this function to set a new page no. on a Page instance. Changes page numbers of other pages in article accordingly. """ old_page_no = self.page_no new_page_no = int(new_page_no) self.page_no = new_page_no self.save() if new_page_no < old_page_no: pages_after = Page.objects.filter(article=self.article, page_no__gte=new_page_no).filter(page_no__lt=old_page_no).exclude(pk=self.id) # print "AAAAAAAAAAAAA" + str(pages_after.count()) for a in pages_after: a.page_no = a.page_no + 1 a.save() else: pages_before = Page.objects.filter(article=self.article, page_no__lte=new_page_no).filter(page_no__gt=old_page_no).exclude(pk=self.id) for b in pages_before: b.page_no = b.page_no - 1 b.save() return self def get_copy(self, new_article): new_page = deepcopy(self) new_page.id = None new_page.article = new_article new_page.save() for i in ImageBox.objects.filter(page=self): new_box = deepcopy(i) new_box.id = None new_box.page = new_page new_box.save() for t in TextBox.objects.filter(page=self): new_box = deepcopy(t) new_box.id = None new_box.page = new_page new_box.save() return new_page def deleteme(self): pages_after = Page.objects.filter(article=self.article, page_no__gt=self.page_no) for p in pages_after: p.page_no = p.page_no - 1 p.save() self.delete() return def get_dict(self, m): """ This function iterates through all boxes on the page and returns a Dict representation which the view converts to json to send to front-end. """ # Image Boxes: imageBoxes = [] for i in ImageBox.objects.filter(page = self).exclude(is_displayed = False): imageBoxes.append(i.to_dict(m)) # Text Boxes: textBoxes = [] for t in TextBox.objects.filter(page = self).exclude(is_displayed = False): textBoxes.append(t.to_dict()) videos = [] for v in self.videos.all(): r = { 'id': v.id, 'title': v.fil.title, 'description': v.fil.description, 'file': v.fil.file.url.replace(PROJECT_PATH, ""), 'mime': 'video' } videos.append(r) audios = [] for a in self.audios.all(): r = { 'id': a.id, 'title': a.fil.title, 'description': a.fil.description, 'file': a.fil.file.url.replace(PROJECT_PATH, ""), 'mime': 'audio' } audios.append(r) ''' #Resources: resources = [] for res in self.resources.all(): r = {} r = { 'file': res.file, 'title': res.title, 'description': res.description, 'tags': res.tags, 'userID': res.userID, 'added': res.added, 'categories': res.categories } resources.append(r) ''' rDict = { 'id': self.id, 'imageBoxes' : imageBoxes, 'textBoxes': textBoxes, 'videos': videos, 'audios': audios # 'resources' : resources } return rDict def save_from_dict(self, d): imageBoxes = d.imageBoxes textBoxes = d.textBoxes for i in imageBoxes: img = ImageBox.objects.get(pk=i.id) img.save_from_dict() for t in textBoxes: txt = TextBox.objects.get(pk=t.id) txt.save_from_dict() #The next two functions are painful. Whoever writes them wins a trip to the moon. def save_revision(self): """ This function saves the json for the page as a Page Revision. """ return True def load_revision(self, rev_no): """ Loads the json data from a revision to populate the page. First, of course, it saves its current state as a revision """ return True def deleteme(self): """ Deletes self - needs to change next page numbers. """ return True class TextBox(models.Model): # Positioning stuff: height = models.IntegerField() width = models.IntegerField() top = models.IntegerField() left = models.IntegerField() # Store reference to file containing text, if any: file = models.ForeignKey(File, blank=True, null=True) # Content, reference to page and whether is_displayed: html = models.TextField() page = models.ForeignKey(Page) is_displayed = models.BooleanField(default=True) # CSS: z_index = models.IntegerField() background_color = models.CharField(max_length=16) border_style = models.CharField(max_length=16) border_width = models.IntegerField() direction = models.CharField(max_length=8) # line_height = models.IntegerField(blank=True, null=True) # letter_spacing = models.IntegerField(blank=True, null=True) # word_spacing = models.IntegerField(blank=True, null=True) border_color = models.CharField(max_length=10) border_radius = models.IntegerField() padding_top = models.IntegerField() padding_left = models.IntegerField() padding_bottom = models.IntegerField() padding_right = models.IntegerField() opacity = models.FloatField() def set_css(self, prop, val): p = prop.replace("-", "_") self.__setattr__(p, cleanCSS(val)) return self def get_css(self, prop): p = prop.replace("-", "_") r = self.__getattribute__(p) return r def to_dict(self): return { 'id': self.id, 'html': self.html, 'css': { 'height': addPx(self.height), 'width': addPx(self.width), 'top': addPx(self.top), 'left': addPx(self.left), 'z-index': self.z_index, 'opacity': self.opacity, 'direction': self.direction, 'border-style': self.border_style, 'border-width': addPx(self.border_width), 'border-color': self.border_color, 'background-color': self.background_color, 'border-radius': addPx(self.border_radius), 'padding-top': addPx(self.padding_top), 'padding-left': addPx(self.padding_left), 'padding-right': addPx(self.padding_right), 'padding-bottom': addPx(self.padding_bottom) } } def save_from_dict(self, d): self.html = d.html self.save() for prop in d.css: self.set_css(prop, d.css[prop]) return self class ImageBox(models.Model): # Positioning stuff: height = models.IntegerField() width = models.IntegerField() top = models.IntegerField() left = models.IntegerField() # Data about the crop: is_cropped = models.BooleanField(default=False) crop_x1 = models.IntegerField(default = 0) crop_x2 = models.IntegerField(default = 0) crop_y1 = models.IntegerField(default = 0) crop_y2 = models.IntegerField(default = 0) # Filename (originally uploaded), reference to page and whether is_displayed: file = models.ForeignKey(File) page = models.ForeignKey(Page) is_displayed = models.BooleanField(default=True) # CSS: z_index = models.IntegerField() border_style = models.CharField(max_length=16) border_width = models.IntegerField() border_color = models.CharField(max_length=10) opacity = models.FloatField() def __unicode__(self): return str(self.id) def set_css(self, prop, val): p = prop.replace("-", "_") self.__setattr__(p, cleanCSS(val)) return self def get_css(self, prop): p = prop.replace("-", "_") r = self.__getattribute__(p) return r def to_dict(self, m): return { 'id': self.id, 'original_print': self.original_print(), # 'original_web': i.original_web(), 'output_web': self.get_path(m), 'css': { 'height': addPx(self.height), 'width': addPx(self.width), 'top': addPx(self.top), 'left': addPx(self.left), 'z-index': int(self.z_index), 'opacity': self.opacity, 'border-style': self.border_style, 'border-width': addPx(self.border_width), 'border-color': self.border_color, }, #DIRTY HACK: Creates a 'resource' thingie to match the front-end droppable behaviour. Try n fix at some point. 'resource': { 'height': self.height * m, 'width': self.width * m, 'resized': self.get_path(m) } } def save_from_dict(self, d): self.html = d.html for prop in d.css: self.set_css(prop, d.css[prop]) return self def from_dict(self, d): self.html = d.html self.crop_top = d.crop_top self.crop_bottom = d.crop_bottom self.crop_left = d.crop_left self.crop_right = d.crop_right self.save() for prop in d.css: self.set_css(prop, d.css[prop]) return self def resize(self, width, height): ''' self.width = width self.height = height self.save() ''' return self #Question: Can the following methods be constructed in such a way that if the filename (based on our naming conventions) exists, it returns the filename, else, it creates it ? """ This function returns the filename of the highest res original dimensions file, converted to jpeg """ def original_print(self): ''' basePath = "media/images/original/" filename = os.path.basename(str(self.file.file)) if extFileName(filename) != 'jpg': f = baseFileName(filename) + ".jpg" else: f = filename ''' return str(self.file.file) def do_crop(self, x1, y1, x2, y2, width, height): tpl = (x1, y1, x2, y2,) image_path = join(MEDIA_ROOT, self.original_print()) cropped = Image.open(image_path).crop(tpl) save_path = join(MEDIA_ROOT, self.cropped_path(), self.cropped_fname()) cropped.save(save_path) return self def crop(self, x1, y1, x2, y2, width, height): original_image = Image.open(join(MEDIA_ROOT, self.actual_unresized())) original_width = original_image.size[0] original_height = original_image.size[1] current_width = self.width current_height = self.height divisor = original_width / (current_width + .0) if self.is_cropped == False: self.crop_x1 = int(round(x1 * divisor)) self.crop_y1 = int(round(y1 * divisor)) self.crop_x2 = int(round(x2 * divisor)) self.crop_y2 = int(round(y2 * divisor)) self.is_cropped = True else: old_x1 = self.crop_x1 old_y1 = self.crop_y1 old_x2 = self.crop_x2 old_y2 = self.crop_y2 self.crop_x1 = int(round(x1 * divisor)) + old_x1 self.crop_y1 = int(round(y1 * divisor)) + old_y1 self.crop_x2 = int(round(x2 * divisor)) + old_x1 self.crop_y2 = int(round(y2 * divisor)) + old_y1 self.width = width self.height = height self.save() tpl = (self.crop_x1, self.crop_y1, self.crop_x2, self.crop_y2,) image_full_path = join(MEDIA_ROOT, self.original_print()) original_cropped = Image.open(image_full_path).crop(tpl) save_path = join(MEDIA_ROOT, self.cropped_path(), self.cropped_fname()) original_cropped.save(save_path) return self def cropped_fname(self): filename = os.path.splitext(os.path.basename(self.original_print()))[0] cropped_fname = "%s_%d_%d_%d_%d.%s" % (filename, self.crop_x1, self.crop_y1, self.crop_x2, self.crop_y2, self.file.ext) return cropped_fname def cropped_path(self): return os.path.dirname(str(self.file.file)) # return MEDIA_ROOT + "/media/images/cropped/" + self.cropped_fname() def actual_unresized(self): if self.is_cropped: return join(self.cropped_path(), self.cropped_fname()) else: return self.original_print() def get_path(self, *args): if len(args) > 0: m = args[0] else: m = 1 path = join(MEDIA_ROOT, self.actual_unresized()) try: f = open(path) except: self.do_crop(self.crop_x1, self.crop_y1, self.crop_x2, self.crop_y2, self.width, self.height) f = open(path) size = str(int(self.width * m)) + "x" + str(int(self.height * m)) im = get_thumbnail(f, size, crop='center', quality=99) ''' context = { 'path': path, # 'size': (self.width * m, self.height * m,) 'size': str(self.width * m) + "x" + str(self.height * m) } ''' return im.url ''' def original_web(self): """ This function returns the filename of the original dimensions file, converted to jpeg, low res for web """ return self def output_print(self): """ This function returns the filename of the high-res cropped file """ return self def output_web(self): """ This function returns the filename of the low-res cropped file """ return self ''' class PageRev(models.Model): """ Stores states of the page (json representations of all boxes on page) with revision numbers so user can revert to a previous state of the page """ page = models.ForeignKey(Page) pickle = models.TextField() rev_no = models.IntegerField() comment = models.TextField(blank=True) def __unicode__(self): return "%s: %s" % (self.page.page_no, self.rev_no) class SliderImage(models.Model): small_image = models.ImageField(upload_to='images/small/', width_field='width', height_field='height', blank=False, null=False) big_image = models.ImageField(upload_to='images/big/', width_field='width', height_field='height', blank=False, null=False) width = models.IntegerField(editable=False) height = models.IntegerField(editable=False) caption = models.CharField(max_length=255, blank=True) def __unicode__(self): return self.caption def comments_notify(sender, **kwargs): comment = kwargs['comment'] name = comment.name email = comment.email content = comment.comment img_id = comment.content_object.id url = "http://edgwareroad.org/slider/%d" % (img_id) message = "Page: %s \n Name: %s \n Email: %s \n Comment: %s" % (url, name, email, content) send_mail("New comment on edgwareroad.org", message, "do_not_reply@edgwareroad.org", ["hello@edgwareroad.org", "sanjaybhangar@gmail.com"]) # f = open("/home/sanj/tmp/edgeTest.txt", "w") # f.write(message) return True comment_was_posted.connect(comments_notify)