def filter_css(self, css):
raise NotImplementedError
+
def filter_js(self, js):
raise NotImplementedError
-
+
+
class FilterError(Exception):
"""
This exception is raised when a filter fails
"""
- pass
\ No newline at end of file
+ pass
warnings.simplefilter('ignore', RuntimeWarning)
+
class CSSTidyFilter(FilterBase):
+
def filter_css(self, css):
tmp_file = tempfile.NamedTemporaryFile(mode='w+b')
tmp_file.write(css)
tmp_file.flush()
output_file = tempfile.NamedTemporaryFile(mode='w+b')
-
+
command = '%s %s %s %s' % (BINARY, tmp_file.name, ARGUMENTS, output_file.name)
-
+
command_output = os.popen(command).read()
-
+
filtered_css = output_file.read()
output_file.close()
tmp_file.close()
-
+
if self.verbose:
print command_output
-
+
return filtered_css
+++ /dev/null
-from django.conf import settings
-
-from compress.filter_base import FilterBase
-from compress.filters.csstidy_python.csstidy import CSSTidy
-
-COMPRESS_CSSTIDY_SETTINGS = getattr(settings, 'COMPRESS_CSSTIDY_SETTINGS', {})
-
-class CSSTidyFilter(FilterBase):
- def filter_css(self, css):
- tidy = CSSTidy()
-
- for k, v in COMPRESS_CSSTIDY_SETTINGS.items():
- tidy.setSetting(k, v)
-
- tidy.parse(css)
-
- r = tidy.Output('string')
-
- return r
+++ /dev/null
-# CSSTidy - CSS Parse
-#
-# CSS Parser class
-#
-# This file is part of CSSTidy.
-#
-# CSSTidy is free software you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation either version 2 of the License, or
-# (at your option) any later version.
-#
-# CSSTidy is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with CSSTidy if not, write to the Free Software
-# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-#
-# @license http://opensource.org/licenses/gpl-license.php GNU Public License
-# @package csstidy
-# @author Dj Gilcrease (digitalxero at gmail dot com) 2005-2006
-
-import re
-
-from optimizer import CSSOptimizer
-from output import CSSPrinter
-import data
-from tools import SortedDict
-
-class CSSTidy(object):
- #Saves the parsed CSS
- _css = ""
- _raw_css = SortedDict()
- _optimized_css = SortedDict()
-
- #List of Tokens
- _tokens = []
-
- #Printer class
- _output = None
-
- #Optimiser class
- _optimizer = None
-
- #Saves the CSS charset (@charset)
- _charset = ''
-
- #Saves all @import URLs
- _import = []
-
- #Saves the namespace
- _namespace = ''
-
- #Contains the version of csstidy
- _version = '1.3'
-
- #Stores the settings
- _settings = {}
-
- # Saves the parser-status.
- #
- # Possible values:
- # - is = in selector
- # - ip = in property
- # - iv = in value
- # - instr = in string (started at " or ' or ( )
- # - ic = in comment (ignore everything)
- # - at = in @-block
- _status = 'is'
-
- #Saves the current at rule (@media)
- _at = ''
-
- #Saves the current selector
- _selector = ''
-
- #Saves the current property
- _property = ''
-
- #Saves the position of , in selectors
- _sel_separate = []
-
- #Saves the current value
- _value = ''
-
- #Saves the current sub-value
- _sub_value = ''
-
- #Saves all subvalues for a property.
- _sub_value_arr = []
-
- #Saves the char which opened the last string
- _str_char = ''
- _cur_string = ''
-
- #Status from which the parser switched to ic or instr
- _from = ''
-
- #Variable needed to manage string-in-strings, for example url("foo.png")
- _str_in_str = False
-
- #=True if in invalid at-rule
- _invalid_at = False
-
- #=True if something has been added to the current selector
- _added = False
-
- #Saves the message log
- _log = SortedDict()
-
- #Saves the line number
- _line = 1
-
- def __init__(self):
- self._settings['remove_bslash'] = True
- self._settings['compress_colors'] = True
- self._settings['compress_font-weight'] = True
- self._settings['lowercase_s'] = False
- self._settings['optimise_shorthands'] = 2
- self._settings['remove_last_'] = False
- self._settings['case_properties'] = 1
- self._settings['sort_properties'] = False
- self._settings['sort_selectors'] = False
- self._settings['merge_selectors'] = 2
- self._settings['discard_invalid_properties'] = False
- self._settings['css_level'] = 'CSS2.1'
- self._settings['preserve_css'] = False
- self._settings['timestamp'] = False
- self._settings['template'] = 'highest_compression'
-
- #Maps self._status to methods
- self.__statusMethod = {'is':self.__parseStatus_is, 'ip': self.__parseStatus_ip, 'iv':self.__parseStatus_iv, 'instr':self.__parseStatus_instr, 'ic':self.__parseStatus_ic, 'at':self.__parseStatus_at}
-
- self._output = CSSPrinter(self)
- self._optimizer = CSSOptimizer(self)
-
- #Public Methods
- def getSetting(self, setting):
- return self._settings.get(setting, False)
-
- #Set the value of a setting.
- def setSetting(self, setting, value):
- self._settings[setting] = value
- return True
-
- def log(self, message, ttype, line = -1):
- if line == -1:
- line = self._line
-
- line = int(line)
-
- add = {'m': message, 't': ttype}
-
- if not self._log.has_key(line):
- self._log[line] = []
- self._log[line].append(add)
- elif add not in self._log[line]:
- self._log[line].append(add)
-
-
- #Checks if a character is escaped (and returns True if it is)
- def escaped(self, string, pos):
- return not (string[pos-1] != '\\' or self.escaped(string, pos-1))
-
- #Adds CSS to an existing media/selector
- def merge_css_blocks(self, media, selector, css_add):
- for prop, value in css_add.iteritems():
- self.__css_add_property(media, selector, prop, value, False)
-
- #Checks if $value is !important.
- def is_important(self, value):
- return '!important' in value.lower()
-
- #Returns a value without !important
- def gvw_important(self, value):
- if self.is_important(value):
- ret = value.strip()
- ret = ret[0:-9]
- ret = ret.strip()
- ret = ret[0:-1]
- ret = ret.strip()
- return ret
-
- return value
-
- def parse(self, cssString):
- #Switch from \r\n to \n
- self._css = cssString.replace("\r\n", "\n") + ' '
- self._raw_css = {}
- self._optimized_css = {}
- self._curComment = ''
-
- #Start Parsing
- i = 0
- while i < len(cssString):
- if self._css[i] == "\n" or self._css[i] == "\r":
- self._line += 1
-
- i += self.__statusMethod[self._status](i)
-
- i += 1;
-
- self._optimized_css = self._optimizer.optimize(self._raw_css)
-
- def parseFile(self, filename):
- try:
- f = open(filename, "r")
- self.parse(f.read())
- finally:
- f.close()
-
- #Private Methods
- def __parseStatus_is(self, idx):
- """
- Parse in Selector
- """
- ret = 0
-
- if self.__is_token(self._css, idx):
- if self._css[idx] == '/' and self._css[idx+1] == '*' and self._selector.strip() == '':
- self._status = 'ic'
- self._from = 'is'
- return 1
-
- elif self._css[idx] == '@' and self._selector.strip() == '':
- #Check for at-rule
- self._invalid_at = True
-
- for name, ttype in data.at_rules.iteritems():
- if self._css[idx+1:len(name)].lower() == name.lower():
- if ttype == 'at':
- self._at = '@' + name
- else:
- self._selector = '@' + name
-
- self._status = ttype
- self._invalid_at = False
- ret += len(name)
-
- if self._invalid_at:
- self._selector = '@'
- invalid_at_name = ''
- for j in xrange(idx+1, len(self._css)):
- if not self._css[j].isalpha():
- break;
-
- invalid_at_name += self._css[j]
-
- self.log('Invalid @-rule: ' + invalid_at_name + ' (removed)', 'Warning')
-
- elif self._css[idx] == '"' or self._css[idx] == "'":
- self._cur_string = self._css[idx]
- self._status = 'instr'
- self._str_char = self._css[idx]
- self._from = 'is'
-
- elif self._invalid_at and self._css[idx] == ';':
- self._invalid_at = False
- self._status = 'is'
-
- elif self._css[idx] == '{':
- self._status = 'ip'
- self.__add_token(data.SEL_START, self._selector)
- self._added = False;
-
- elif self._css[idx] == '}':
- self.__add_token(data.AT_END, self._at)
- self._at = ''
- self._selector = ''
- self._sel_separate = []
-
- elif self._css[idx] == ',':
- self._selector = self._selector.strip() + ','
- self._sel_separate.append(len(self._selector))
-
- elif self._css[idx] == '\\':
- self._selector += self.__unicode(idx)
-
- #remove unnecessary universal selector, FS#147
- elif not (self._css[idx] == '*' and self._css[idx+1] in ('.', '#', '[', ':')):
- self._selector += self._css[idx]
-
- else:
- lastpos = len(self._selector)-1
-
- if lastpos == -1 or not ((self._selector[lastpos].isspace() or self.__is_token(self._selector, lastpos) and self._selector[lastpos] == ',') and self._css[idx].isspace()):
- self._selector += self._css[idx]
-
- return ret
-
- def __parseStatus_ip(self, idx):
- """
- Parse in property
- """
- if self.__is_token(self._css, idx):
- if (self._css[idx] == ':' or self._css[idx] == '=') and self._property != '':
- self._status = 'iv'
-
- if not self.getSetting('discard_invalid_properties') or self.__property_is_valid(self._property):
- self.__add_token(data.PROPERTY, self._property)
-
- elif self._css[idx] == '/' and self._css[idx+1] == '*' and self._property == '':
- self._status = 'ic'
- self._from = 'ip'
- return 1
-
- elif self._css[idx] == '}':
- self.__explode_selectors()
- self._status = 'is'
- self._invalid_at = False
- self.__add_token(data.SEL_END, self._selector)
- self._selector = ''
- self._property = ''
-
- elif self._css[idx] == ';':
- self._property = ''
-
- elif self._css[idx] == '\\':
- self._property += self.__unicode(idx)
-
- elif not self._css[idx].isspace():
- self._property += self._css[idx]
-
- return 0
-
- def __parseStatus_iv(self, idx):
- """
- Parse in value
- """
- pn = (( self._css[idx] == "\n" or self._css[idx] == "\r") and self.__property_is_next(idx+1) or idx == len(self._css)) #CHECK#
- if self.__is_token(self._css, idx) or pn:
- if self._css[idx] == '/' and self._css[idx+1] == '*':
- self._status = 'ic'
- self._from = 'iv'
- return 1
-
- elif self._css[idx] == '"' or self._css[idx] == "'" or self._css[idx] == '(':
- self._cur_string = self._css[idx]
- self._str_char = ')' if self._css[idx] == '(' else self._css[idx]
- self._status = 'instr'
- self._from = 'iv'
-
- elif self._css[idx] == ',':
- self._sub_value = self._sub_value.strip() + ','
-
- elif self._css[idx] == '\\':
- self._sub_value += self.__unicode(idx)
-
- elif self._css[idx] == ';' or pn:
- if len(self._selector) > 0 and self._selector[0] == '@' and data.at_rules.has_key(self._selector[1:]) and data.at_rules[self._selector[1:]] == 'iv':
- self._sub_value_arr.append(self._sub_value.strip())
-
- self._status = 'is'
-
- if '@charset' in self._selector:
- self._charset = self._sub_value_arr[0]
-
- elif '@namespace' in self._selector:
- self._namespace = ' '.join(self._sub_value_arr)
-
- elif '@import' in self._selector:
- self._import.append(' '.join(self._sub_value_arr))
-
-
- self._sub_value_arr = []
- self._sub_value = ''
- self._selector = ''
- self._sel_separate = []
-
- else:
- self._status = 'ip'
-
- elif self._css[idx] != '}':
- self._sub_value += self._css[idx]
-
- if (self._css[idx] == '}' or self._css[idx] == ';' or pn) and self._selector != '':
- if self._at == '':
- self._at = data.DEFAULT_AT
-
- #case settings
- if self.getSetting('lowercase_s'):
- self._selector = self._selector.lower()
-
- self._property = self._property.lower()
-
- if self._sub_value != '':
- self._sub_value_arr.append(self._sub_value)
- self._sub_value = ''
-
- self._value = ' '.join(self._sub_value_arr)
-
-
- self._selector = self._selector.strip()
-
- valid = self.__property_is_valid(self._property)
-
- if (not self._invalid_at or self.getSetting('preserve_css')) and (not self.getSetting('discard_invalid_properties') or valid):
- self.__css_add_property(self._at, self._selector, self._property, self._value)
- self.__add_token(data.VALUE, self._value)
-
- if not valid:
- if self.getSetting('discard_invalid_properties'):
- self.log('Removed invalid property: ' + self._property, 'Warning')
-
- else:
- self.log('Invalid property in ' + self.getSetting('css_level').upper() + ': ' + self._property, 'Warning')
-
- self._property = '';
- self._sub_value_arr = []
- self._value = ''
-
- if self._css[idx] == '}':
- self.__explode_selectors()
- self.__add_token(data.SEL_END, self._selector)
- self._status = 'is'
- self._invalid_at = False
- self._selector = ''
-
- elif not pn:
- self._sub_value += self._css[idx]
-
- if self._css[idx].isspace():
- if self._sub_value != '':
- self._sub_value_arr.append(self._sub_value)
- self._sub_value = ''
-
- return 0
-
- def __parseStatus_instr(self, idx):
- """
- Parse in String
- """
- if self._str_char == ')' and (self._css[idx] == '"' or self._css[idx] == "'") and not self.escaped(self._css, idx):
- self._str_in_str = not self._str_in_str
-
- temp_add = self._css[idx] # ...and no not-escaped backslash at the previous position
- if (self._css[idx] == "\n" or self._css[idx] == "\r") and not (self._css[idx-1] == '\\' and not self.escaped(self._css, idx-1)):
- temp_add = "\\A "
- self.log('Fixed incorrect newline in string', 'Warning')
-
- if not (self._str_char == ')' and self._css[idx].isspace() and not self._str_in_str):
- self._cur_string += temp_add
-
- if self._css[idx] == self._str_char and not self.escaped(self._css, idx) and not self._str_in_str:
- self._status = self._from
- regex = re.compile(r'([\s]+)', re.I | re.U | re.S)
- if regex.match(self._cur_string) is None and self._property != 'content':
- if self._str_char == '"' or self._str_char == "'":
- self._cur_string = self._cur_string[1:-1]
-
- elif len(self._cur_string) > 3 and (self._cur_string[1] == '"' or self._cur_string[1] == "'"):
- self._cur_string = self._cur_string[0] + self._cur_string[2:-2] + self._cur_string[-1]
-
- if self._from == 'iv':
- self._sub_value += self._cur_string
-
- elif self._from == 'is':
- self._selector += self._cur_string
-
- return 0
-
- def __parseStatus_ic(self, idx):
- """
- Parse css In Comment
- """
- if self._css[idx] == '*' and self._css[idx+1] == '/':
- self._status = self._from
- self.__add_token(data.COMMENT, self._curComment)
- self._curComment = ''
- return 1
-
- else:
- self._curComment += self._css[idx]
-
- return 0
-
- def __parseStatus_at(self, idx):
- """
- Parse in at-block
- """
- if self.__is_token(string, idx):
- if self._css[idx] == '/' and self._css[idx+1] == '*':
- self._status = 'ic'
- self._from = 'at'
- return 1
-
- elif self._css[i] == '{':
- self._status = 'is'
- self.__add_token(data.AT_START, self._at)
-
- elif self._css[i] == ',':
- self._at = self._at.strip() + ','
-
- elif self._css[i] == '\\':
- self._at += self.__unicode(i)
- else:
- lastpos = len(self._at)-1
- if not (self._at[lastpos].isspace() or self.__is_token(self._at, lastpos) and self._at[lastpos] == ',') and self._css[i].isspace():
- self._at += self._css[i]
-
- return 0
-
- def __explode_selectors(self):
- #Explode multiple selectors
- if self.getSetting('merge_selectors') == 1:
- new_sels = []
- lastpos = 0;
- self._sel_separate.append(len(self._selector))
-
- for num in xrange(len(self._sel_separate)):
- pos = self._sel_separate[num]
- if num == (len(self._sel_separate)): #CHECK#
- pos += 1
-
- new_sels.append(self._selector[lastpos:(pos-lastpos-1)])
- lastpos = pos
-
- if len(new_sels) > 1:
- for selector in new_sels:
- self.merge_css_blocks(self._at, selector, self._raw_css[self._at][self._selector])
-
- del self._raw_css[self._at][self._selector]
-
- self._sel_separate = []
-
- #Adds a property with value to the existing CSS code
- def __css_add_property(self, media, selector, prop, new_val):
- if self.getSetting('preserve_css') or new_val.strip() == '':
- return
-
- if not self._raw_css.has_key(media):
- self._raw_css[media] = SortedDict()
-
- if not self._raw_css[media].has_key(selector):
- self._raw_css[media][selector] = SortedDict()
-
- self._added = True
- if self._raw_css[media][selector].has_key(prop):
- if (self.is_important(self._raw_css[media][selector][prop]) and self.is_important(new_val)) or not self.is_important(self._raw_css[media][selector][prop]):
- del self._raw_css[media][selector][prop]
- self._raw_css[media][selector][prop] = new_val.strip()
-
- else:
- self._raw_css[media][selector][prop] = new_val.strip()
-
- #Checks if the next word in a string from pos is a CSS property
- def __property_is_next(self, pos):
- istring = self._css[pos: len(self._css)]
- pos = istring.find(':')
- if pos == -1:
- return False;
-
- istring = istring[:pos].strip().lower()
- if data.all_properties.has_key(istring):
- self.log('Added semicolon to the end of declaration', 'Warning')
- return True
-
- return False;
-
- #Checks if a property is valid
- def __property_is_valid(self, prop):
- return (data.all_properties.has_key(prop) and data.all_properties[prop].find(self.getSetting('css_level').upper()) != -1)
-
- #Adds a token to self._tokens
- def __add_token(self, ttype, cssdata, do=False):
- if self.getSetting('preserve_css') or do:
- if ttype == data.COMMENT:
- token = [ttype, cssdata]
- else:
- token = [ttype, cssdata.strip()]
-
- self._tokens.append(token)
-
- #Parse unicode notations and find a replacement character
- def __unicode(self, idx):
- ##FIX##
- return ''
-
- #Starts parsing from URL
- ##USED?
- def __parse_from_url(self, url):
- try:
- if "http" in url.lower() or "https" in url.lower():
- f = urllib.urlopen(url)
- else:
- f = open(url)
-
- data = f.read()
- return self.parse(data)
- finally:
- f.close()
-
- #Checks if there is a token at the current position
- def __is_token(self, string, idx):
- return (string[idx] in data.tokens and not self.escaped(string, idx))
-
-
- #Property Methods
- def _getOutput(self):
- self._output.prepare(self._optimized_css)
- return self._output.render
-
- def _getLog(self):
- ret = ""
- ks = self._log.keys()
- ks.sort()
- for line in ks:
- for msg in self._log[line]:
- ret += "Type: " + msg['t'] + "\n"
- ret += "Message: " + msg['m'] + "\n"
- ret += "\n"
-
- return ret
-
- def _getCSS(self):
- return self._css
-
-
- #Properties
- Output = property(_getOutput, None)
- Log = property(_getLog, None)
- CSS = property(_getCSS, None)
-
-
-if __name__ == '__main__':
- import sys
- tidy = CSSTidy()
- f = open(sys.argv[1], "r")
- css = f.read()
- f.close()
- tidy.parse(css)
- tidy.Output('file', filename="Stylesheet.min.css")
- print tidy.Output()
- #print tidy._import
\ No newline at end of file
+++ /dev/null
-# Various CSS Data for CSSTidy
-#
-# This file is part of CSSTidy.
-#
-# CSSTidy is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# CSSTidy is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with CSSTidy; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-#
-# @license http://opensource.org/licenses/gpl-license.php GNU Public License
-# @package csstidy
-# @author Florian Schmitz (floele at gmail dot com) 2005
-
-AT_START = 1
-AT_END = 2
-SEL_START = 3
-SEL_END = 4
-PROPERTY = 5
-VALUE = 6
-COMMENT = 7
-DEFAULT_AT = 41
-
-# All whitespace allowed in CSS
-#
-# @global array whitespace
-# @version 1.0
-whitespace = frozenset([' ',"\n","\t","\r","\x0B"])
-
-# All CSS tokens used by csstidy
-#
-# @global string tokens
-# @version 1.0
-tokens = '/@}{;:=\'"(,\\!$%&)#+.<>?[]^`|~'
-
-# All CSS units (CSS 3 units included)
-#
-# @see compress_numbers()
-# @global array units
-# @version 1.0
-units = frozenset(['in','cm','mm','pt','pc','px','rem','em','%','ex','gd','vw','vh','vm','deg','grad','rad','ms','s','khz','hz'])
-
-# Available at-rules
-#
-# @global array at_rules
-# @version 1.0
-at_rules = {'page':'is', 'font-face':'is', 'charset':'iv', 'import':'iv', 'namespace':'iv', 'media':'at'}
-
-# Properties that need a value with unit
-#
-# @todo CSS3 properties
-# @see compress_numbers()
-# @global array unit_values
-# @version 1.2
-unit_values = frozenset(['background', 'background-position', 'border', 'border-top', 'border-right', 'border-bottom',
- 'border-left', 'border-width', 'border-top-width', 'border-right-width', 'border-left-width',
- 'border-bottom-width', 'bottom', 'border-spacing', 'font-size','height', 'left', 'margin', 'margin-top',
- 'margin-right', 'margin-bottom', 'margin-left', 'max-height', 'max-width', 'min-height', 'min-width',
- 'outline-width', 'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left','position',
- 'right', 'top', 'text-indent', 'letter-spacing', 'word-spacing', 'width'
- ])
-
-
-# Properties that allow <color> as value
-#
-# @todo CSS3 properties
-# @see compress_numbers()
-# @global array color_values
-# @version 1.0
-color_values = frozenset(['background-color', 'border-color', 'border-top-color', 'border-right-color',
- 'border-bottom-color', 'border-left-color', 'color', 'outline-color'])
-
-
-# Default values for the background properties
-#
-# @todo Possibly property names will change during CSS3 development
-# @global array background_prop_default
-# @see dissolve_short_bg()
-# @see merge_bg()
-# @version 1.0
-background_prop_default = {}
-background_prop_default['background-image'] = 'none'
-background_prop_default['background-size'] = 'auto'
-background_prop_default['background-repeat'] = 'repeat'
-background_prop_default['background-position'] = '0 0'
-background_prop_default['background-attachment'] = 'scroll'
-background_prop_default['background-clip'] = 'border'
-background_prop_default['background-origin'] = 'padding'
-background_prop_default['background-color'] = 'transparent'
-
-# A list of non-W3C color names which get replaced by their hex-codes
-#
-# @global array replace_colors
-# @see cut_color()
-# @version 1.0
-replace_colors = {}
-replace_colors['aliceblue'] = '#F0F8FF'
-replace_colors['antiquewhite'] = '#FAEBD7'
-replace_colors['aquamarine'] = '#7FFFD4'
-replace_colors['azure'] = '#F0FFFF'
-replace_colors['beige'] = '#F5F5DC'
-replace_colors['bisque'] = '#FFE4C4'
-replace_colors['blanchedalmond'] = '#FFEBCD'
-replace_colors['blueviolet'] = '#8A2BE2'
-replace_colors['brown'] = '#A52A2A'
-replace_colors['burlywood'] = '#DEB887'
-replace_colors['cadetblue'] = '#5F9EA0'
-replace_colors['chartreuse'] = '#7FFF00'
-replace_colors['chocolate'] = '#D2691E'
-replace_colors['coral'] = '#FF7F50'
-replace_colors['cornflowerblue'] = '#6495ED'
-replace_colors['cornsilk'] = '#FFF8DC'
-replace_colors['crimson'] = '#DC143C'
-replace_colors['cyan'] = '#00FFFF'
-replace_colors['darkblue'] = '#00008B'
-replace_colors['darkcyan'] = '#008B8B'
-replace_colors['darkgoldenrod'] = '#B8860B'
-replace_colors['darkgray'] = '#A9A9A9'
-replace_colors['darkgreen'] = '#006400'
-replace_colors['darkkhaki'] = '#BDB76B'
-replace_colors['darkmagenta'] = '#8B008B'
-replace_colors['darkolivegreen'] = '#556B2F'
-replace_colors['darkorange'] = '#FF8C00'
-replace_colors['darkorchid'] = '#9932CC'
-replace_colors['darkred'] = '#8B0000'
-replace_colors['darksalmon'] = '#E9967A'
-replace_colors['darkseagreen'] = '#8FBC8F'
-replace_colors['darkslateblue'] = '#483D8B'
-replace_colors['darkslategray'] = '#2F4F4F'
-replace_colors['darkturquoise'] = '#00CED1'
-replace_colors['darkviolet'] = '#9400D3'
-replace_colors['deeppink'] = '#FF1493'
-replace_colors['deepskyblue'] = '#00BFFF'
-replace_colors['dimgray'] = '#696969'
-replace_colors['dodgerblue'] = '#1E90FF'
-replace_colors['feldspar'] = '#D19275'
-replace_colors['firebrick'] = '#B22222'
-replace_colors['floralwhite'] = '#FFFAF0'
-replace_colors['forestgreen'] = '#228B22'
-replace_colors['gainsboro'] = '#DCDCDC'
-replace_colors['ghostwhite'] = '#F8F8FF'
-replace_colors['gold'] = '#FFD700'
-replace_colors['goldenrod'] = '#DAA520'
-replace_colors['greenyellow'] = '#ADFF2F'
-replace_colors['honeydew'] = '#F0FFF0'
-replace_colors['hotpink'] = '#FF69B4'
-replace_colors['indianred'] = '#CD5C5C'
-replace_colors['indigo'] = '#4B0082'
-replace_colors['ivory'] = '#FFFFF0'
-replace_colors['khaki'] = '#F0E68C'
-replace_colors['lavender'] = '#E6E6FA'
-replace_colors['lavenderblush'] = '#FFF0F5'
-replace_colors['lawngreen'] = '#7CFC00'
-replace_colors['lemonchiffon'] = '#FFFACD'
-replace_colors['lightblue'] = '#ADD8E6'
-replace_colors['lightcoral'] = '#F08080'
-replace_colors['lightcyan'] = '#E0FFFF'
-replace_colors['lightgoldenrodyellow'] = '#FAFAD2'
-replace_colors['lightgrey'] = '#D3D3D3'
-replace_colors['lightgreen'] = '#90EE90'
-replace_colors['lightpink'] = '#FFB6C1'
-replace_colors['lightsalmon'] = '#FFA07A'
-replace_colors['lightseagreen'] = '#20B2AA'
-replace_colors['lightskyblue'] = '#87CEFA'
-replace_colors['lightslateblue'] = '#8470FF'
-replace_colors['lightslategray'] = '#778899'
-replace_colors['lightsteelblue'] = '#B0C4DE'
-replace_colors['lightyellow'] = '#FFFFE0'
-replace_colors['limegreen'] = '#32CD32'
-replace_colors['linen'] = '#FAF0E6'
-replace_colors['magenta'] = '#FF00FF'
-replace_colors['mediumaquamarine'] = '#66CDAA'
-replace_colors['mediumblue'] = '#0000CD'
-replace_colors['mediumorchid'] = '#BA55D3'
-replace_colors['mediumpurple'] = '#9370D8'
-replace_colors['mediumseagreen'] = '#3CB371'
-replace_colors['mediumslateblue'] = '#7B68EE'
-replace_colors['mediumspringgreen'] = '#00FA9A'
-replace_colors['mediumturquoise'] = '#48D1CC'
-replace_colors['mediumvioletred'] = '#C71585'
-replace_colors['midnightblue'] = '#191970'
-replace_colors['mintcream'] = '#F5FFFA'
-replace_colors['mistyrose'] = '#FFE4E1'
-replace_colors['moccasin'] = '#FFE4B5'
-replace_colors['navajowhite'] = '#FFDEAD'
-replace_colors['oldlace'] = '#FDF5E6'
-replace_colors['olivedrab'] = '#6B8E23'
-replace_colors['orangered'] = '#FF4500'
-replace_colors['orchid'] = '#DA70D6'
-replace_colors['palegoldenrod'] = '#EEE8AA'
-replace_colors['palegreen'] = '#98FB98'
-replace_colors['paleturquoise'] = '#AFEEEE'
-replace_colors['palevioletred'] = '#D87093'
-replace_colors['papayawhip'] = '#FFEFD5'
-replace_colors['peachpuff'] = '#FFDAB9'
-replace_colors['peru'] = '#CD853F'
-replace_colors['pink'] = '#FFC0CB'
-replace_colors['plum'] = '#DDA0DD'
-replace_colors['powderblue'] = '#B0E0E6'
-replace_colors['rosybrown'] = '#BC8F8F'
-replace_colors['royalblue'] = '#4169E1'
-replace_colors['saddlebrown'] = '#8B4513'
-replace_colors['salmon'] = '#FA8072'
-replace_colors['sandybrown'] = '#F4A460'
-replace_colors['seagreen'] = '#2E8B57'
-replace_colors['seashell'] = '#FFF5EE'
-replace_colors['sienna'] = '#A0522D'
-replace_colors['skyblue'] = '#87CEEB'
-replace_colors['slateblue'] = '#6A5ACD'
-replace_colors['slategray'] = '#708090'
-replace_colors['snow'] = '#FFFAFA'
-replace_colors['springgreen'] = '#00FF7F'
-replace_colors['steelblue'] = '#4682B4'
-replace_colors['tan'] = '#D2B48C'
-replace_colors['thistle'] = '#D8BFD8'
-replace_colors['tomato'] = '#FF6347'
-replace_colors['turquoise'] = '#40E0D0'
-replace_colors['violet'] = '#EE82EE'
-replace_colors['violetred'] = '#D02090'
-replace_colors['wheat'] = '#F5DEB3'
-replace_colors['whitesmoke'] = '#F5F5F5'
-replace_colors['yellowgreen'] = '#9ACD32'
-
-#A list of optimized colors
-optimize_colors = {}
-optimize_colors['black'] = '#000'
-optimize_colors['fuchsia'] = '#F0F'
-optimize_colors['white'] = '#FFF'
-optimize_colors['yellow'] = '#FF0'
-optimize_colors['cyan'] = '#0FF'
-optimize_colors['magenta'] = '#F0F'
-optimize_colors['lightslategray'] = '#789'
-
-optimize_colors['#800000'] = 'maroon'
-optimize_colors['#FFA500'] = 'orange'
-optimize_colors['#808000'] = 'olive'
-optimize_colors['#800080'] = 'purple'
-optimize_colors['#008000'] = 'green'
-optimize_colors['#000080'] = 'navy'
-optimize_colors['#008080'] = 'teal'
-optimize_colors['#C0C0C0'] = 'silver'
-optimize_colors['#808080'] = 'gray'
-optimize_colors['#4B0082'] = 'indigo'
-optimize_colors['#FFD700'] = 'gold'
-optimize_colors['#A52A2A'] = 'brown'
-optimize_colors['#00FFFF'] = 'cyan'
-optimize_colors['#EE82EE'] = 'violet'
-optimize_colors['#DA70D6'] = 'orchid'
-optimize_colors['#FFE4C4'] = 'bisque'
-optimize_colors['#F0E68C'] = 'khaki'
-optimize_colors['#F5DEB3'] = 'wheat'
-optimize_colors['#FF7F50'] = 'coral'
-optimize_colors['#F5F5DC'] = 'beige'
-optimize_colors['#F0FFFF'] = 'azure'
-optimize_colors['#A0522D'] = 'sienna'
-optimize_colors['#CD853F'] = 'peru'
-optimize_colors['#FFFFF0'] = 'ivory'
-optimize_colors['#DDA0DD'] = 'plum'
-optimize_colors['#D2B48C'] = 'tan'
-optimize_colors['#FFC0CB'] = 'pink'
-optimize_colors['#FFFAFA'] = 'snow'
-optimize_colors['#FA8072'] = 'salmon'
-optimize_colors['#FF6347'] = 'tomato'
-optimize_colors['#FAF0E6'] = 'linen'
-optimize_colors['#F00'] = 'red'
-
-
-# A list of all shorthand properties that are devided into four properties and/or have four subvalues
-#
-# @global array shorthands
-# @todo Are there new ones in CSS3?
-# @see dissolve_4value_shorthands()
-# @see merge_4value_shorthands()
-# @version 1.0
-shorthands = {}
-shorthands['border-color'] = ['border-top-color','border-right-color','border-bottom-color','border-left-color']
-shorthands['border-style'] = ['border-top-style','border-right-style','border-bottom-style','border-left-style']
-shorthands['border-width'] = ['border-top-width','border-right-width','border-bottom-width','border-left-width']
-shorthands['margin'] = ['margin-top','margin-right','margin-bottom','margin-left']
-shorthands['padding'] = ['padding-top','padding-right','padding-bottom','padding-left']
-shorthands['-moz-border-radius'] = 0
-
-# All CSS Properties. Needed for csstidy::property_is_next()
-#
-# @global array all_properties
-# @todo Add CSS3 properties
-# @version 1.0
-# @see csstidy::property_is_next()
-all_properties = {}
-all_properties['background'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['background-color'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['background-image'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['background-repeat'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['background-attachment'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['background-position'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-top'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-right'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-bottom'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-left'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-color'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-top-color'] = 'CSS2.0,CSS2.1'
-all_properties['border-bottom-color'] = 'CSS2.0,CSS2.1'
-all_properties['border-left-color'] = 'CSS2.0,CSS2.1'
-all_properties['border-right-color'] = 'CSS2.0,CSS2.1'
-all_properties['border-style'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-top-style'] = 'CSS2.0,CSS2.1'
-all_properties['border-right-style'] = 'CSS2.0,CSS2.1'
-all_properties['border-left-style'] = 'CSS2.0,CSS2.1'
-all_properties['border-bottom-style'] = 'CSS2.0,CSS2.1'
-all_properties['border-width'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-top-width'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-right-width'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-left-width'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-bottom-width'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-collapse'] = 'CSS2.0,CSS2.1'
-all_properties['border-spacing'] = 'CSS2.0,CSS2.1'
-all_properties['bottom'] = 'CSS2.0,CSS2.1'
-all_properties['caption-side'] = 'CSS2.0,CSS2.1'
-all_properties['content'] = 'CSS2.0,CSS2.1'
-all_properties['clear'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['clip'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['color'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['counter-reset'] = 'CSS2.0,CSS2.1'
-all_properties['counter-increment'] = 'CSS2.0,CSS2.1'
-all_properties['cursor'] = 'CSS2.0,CSS2.1'
-all_properties['empty-cells'] = 'CSS2.0,CSS2.1'
-all_properties['display'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['direction'] = 'CSS2.0,CSS2.1'
-all_properties['float'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['font'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['font-family'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['font-style'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['font-variant'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['font-weight'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['font-stretch'] = 'CSS2.0'
-all_properties['font-size-adjust'] = 'CSS2.0'
-all_properties['font-size'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['height'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['left'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['line-height'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['list-style'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['list-style-type'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['list-style-image'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['list-style-position'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['margin'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['margin-top'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['margin-right'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['margin-bottom'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['margin-left'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['marks'] = 'CSS1.0,CSS2.0'
-all_properties['marker-offset'] = 'CSS2.0'
-all_properties['max-height'] = 'CSS2.0,CSS2.1'
-all_properties['max-width'] = 'CSS2.0,CSS2.1'
-all_properties['min-height'] = 'CSS2.0,CSS2.1'
-all_properties['min-width'] = 'CSS2.0,CSS2.1'
-all_properties['overflow'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['orphans'] = 'CSS2.0,CSS2.1'
-all_properties['outline'] = 'CSS2.0,CSS2.1'
-all_properties['outline-width'] = 'CSS2.0,CSS2.1'
-all_properties['outline-style'] = 'CSS2.0,CSS2.1'
-all_properties['outline-color'] = 'CSS2.0,CSS2.1'
-all_properties['padding'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['padding-top'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['padding-right'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['padding-bottom'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['padding-left'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['page-break-before'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['page-break-after'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['page-break-inside'] = 'CSS2.0,CSS2.1'
-all_properties['page'] = 'CSS2.0'
-all_properties['position'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['quotes'] = 'CSS2.0,CSS2.1'
-all_properties['right'] = 'CSS2.0,CSS2.1'
-all_properties['size'] = 'CSS1.0,CSS2.0'
-all_properties['speak-header'] = 'CSS2.0,CSS2.1'
-all_properties['table-layout'] = 'CSS2.0,CSS2.1'
-all_properties['top'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['text-indent'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['text-align'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['text-decoration'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['text-shadow'] = 'CSS2.0'
-all_properties['letter-spacing'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['word-spacing'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['text-transform'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['white-space'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['unicode-bidi'] = 'CSS2.0,CSS2.1'
-all_properties['vertical-align'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['visibility'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['width'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['widows'] = 'CSS2.0,CSS2.1'
-all_properties['z-index'] = 'CSS1.0,CSS2.0,CSS2.1'
-
-# Speech #
-all_properties['volume'] = 'CSS2.0,CSS2.1'
-all_properties['speak'] = 'CSS2.0,CSS2.1'
-all_properties['pause'] = 'CSS2.0,CSS2.1'
-all_properties['pause-before'] = 'CSS2.0,CSS2.1'
-all_properties['pause-after'] = 'CSS2.0,CSS2.1'
-all_properties['cue'] = 'CSS2.0,CSS2.1'
-all_properties['cue-before'] = 'CSS2.0,CSS2.1'
-all_properties['cue-after'] = 'CSS2.0,CSS2.1'
-all_properties['play-during'] = 'CSS2.0,CSS2.1'
-all_properties['azimuth'] = 'CSS2.0,CSS2.1'
-all_properties['elevation'] = 'CSS2.0,CSS2.1'
-all_properties['speech-rate'] = 'CSS2.0,CSS2.1'
-all_properties['voice-family'] = 'CSS2.0,CSS2.1'
-all_properties['pitch'] = 'CSS2.0,CSS2.1'
-all_properties['pitch-range'] = 'CSS2.0,CSS2.1'
-all_properties['stress'] = 'CSS2.0,CSS2.1'
-all_properties['richness'] = 'CSS2.0,CSS2.1'
-all_properties['speak-punctuation'] = 'CSS2.0,CSS2.1'
-all_properties['speak-numeral'] = 'CSS2.0,CSS2.1'
\ No newline at end of file
+++ /dev/null
-# CSSTidy - CSS Optimizer
-#
-# CSS Optimizer class
-#
-# This file is part of CSSTidy.
-#
-# CSSTidy is free software you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation either version 2 of the License, or
-# (at your option) any later version.
-#
-# CSSTidy is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with CSSTidy if not, write to the Free Software
-# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-#
-# @license http://opensource.org/licenses/gpl-license.php GNU Public License
-# @package csstidy
-# @author Dj Gilcrease (digitalxero at gmail dot com) 2005-2006
-
-import data
-from tools import SortedDict
-
-
-class CSSOptimizer(object):
- def __init__(self, parser):
- #raw_css is a dict
- self.parser = parser
- self._optimized_css = SortedDict
-
-
-#PUBLIC METHODS
- def optimize(self, raw_css):
- if self.parser.getSetting('preserve_css'):
- return raw_css
-
- self._optimized_css = raw_css
-
- if self.parser.getSetting('merge_selectors') == 2:
- self.__merge_selectors()
-
- ##OPTIMIZE##
- for media, css in self._optimized_css.iteritems():
- for selector, cssdata in css.iteritems():
- if self.parser.getSetting('optimise_shorthands') >= 1:
- cssdata = self.__merge_4value_shorthands(cssdata)
-
- if self.parser.getSetting('optimise_shorthands') >= 2:
- cssdata = self.__merge_bg(cssdata)
-
- for item, value in cssdata.iteritems():
- value = self.__compress_numbers(item, value)
- value = self.__compress_important(value)
-
- if item in data.color_values and self.parser.getSetting('compress_colors'):
- old = value[:]
- value = self.__compress_color(value)
- if old != value:
- self.parser.log('In "' + selector + '" Optimised ' + item + ': Changed ' + old + ' to ' + value, 'Information')
-
- if item == 'font-weight' and self.parser.getSetting('compress_font-weight'):
- if value == 'bold':
- value = '700'
- self.parser.log('In "' + selector + '" Optimised font-weight: Changed "bold" to "700"', 'Information')
-
- elif value == 'normal':
- value = '400'
- self.parser.log('In "' + selector + '" Optimised font-weight: Changed "normal" to "400"', 'Information')
-
- self._optimized_css[media][selector][item] = value
-
-
- return self._optimized_css
-
-
-#PRIVATE METHODS
- def __merge_bg(self, cssdata):
- """
- Merges all background properties
- @cssdata (dict) is a dictionary of the selector properties
- """
- #Max number of background images. CSS3 not yet fully implemented
- img = 1
- clr = 1
- bg_img_list = []
- if cssdata.has_key('background-image'):
- img = len(cssdata['background-image'].split(','))
- bg_img_list = self.parser.gvw_important(cssdata['background-image']).split(',')
-
- elif cssdata.has_key('background-color'):
- clr = len(cssdata['background-color'].split(','))
-
-
- number_of_values = max(img, clr, 1)
-
- new_bg_value = ''
- important = ''
-
- for i in xrange(number_of_values):
- for bg_property, default_value in data.background_prop_default.iteritems():
- #Skip if property does not exist
- if not cssdata.has_key(bg_property):
- continue
-
- cur_value = cssdata[bg_property]
-
- #Skip some properties if there is no background image
- if (len(bg_img_list) > i and bg_img_list[i] == 'none') and bg_property in frozenset(['background-size', 'background-position', 'background-attachment', 'background-repeat']):
- continue
-
- #Remove !important
- if self.parser.is_important(cur_value):
- important = ' !important'
- cur_value = self.parser.gvw_important(cur_value)
-
- #Do not add default values
- if cur_value == default_value:
- continue
-
- temp = cur_value.split(',')
-
- if len(temp) > i:
- if bg_property == 'background-size':
- new_bg_value += '(' + temp[i] + ') '
-
- else:
- new_bg_value += temp[i] + ' '
-
- new_bg_value = new_bg_value.strip()
- if i != (number_of_values-1):
- new_bg_value += ','
-
- #Delete all background-properties
- for bg_property, default_value in data.background_prop_default.iteritems():
- try:
- del cssdata[bg_property]
- except:
- pass
-
- #Add new background property
- if new_bg_value != '':
- cssdata['background'] = new_bg_value + important
-
- return cssdata
-
- def __merge_4value_shorthands(self, cssdata):
- """
- Merges Shorthand properties again, the opposite of dissolve_4value_shorthands()
- @cssdata (dict) is a dictionary of the selector properties
- """
- for key, value in data.shorthands.iteritems():
- important = ''
- if value != 0 and cssdata.has_key(value[0]) and cssdata.has_key(value[1]) and cssdata.has_key(value[2]) and cssdata.has_key(value[3]):
- cssdata[key] = ''
-
- for i in xrange(4):
- val = cssdata[value[i]]
- if self.parser.is_important(val):
- important = '!important'
- cssdata[key] += self.parser.gvw_important(val) + ' '
-
- else:
- cssdata[key] += val + ' '
-
- del cssdata[value[i]]
- if cssdata.has_key(key):
- cssdata[key] = self.__shorthand(cssdata[key] + important.strip())
-
- return cssdata
-
-
- def __merge_selectors(self):
- """
- Merges selectors with same properties. Example: a{color:red} b{color:red} . a,b{color:red}
- Very basic and has at least one bug. Hopefully there is a replacement soon.
- @selector_one (string) is the current selector
- @value_one (dict) is a dictionary of the selector properties
- Note: Currently is the elements of a selector are identical, but in a different order, they are not merged
- """
-
- ##OPTIMIZE##
- ##FIX##
-
- raw_css = self._optimized_css.copy()
- delete = []
- add = SortedDict()
- for media, css in raw_css.iteritems():
- for selector_one, value_one in css.iteritems():
- newsel = selector_one
-
- for selector_two, value_two in css.iteritems():
- if selector_one == selector_two:
- #We need to skip self
- continue
-
- if value_one == value_two:
- #Ok, we need to merge these two selectors
- newsel += ', ' + selector_two
- delete.append((media, selector_two))
-
-
- if not add.has_key(media):
- add[media] = SortedDict()
-
- add[media][newsel] = value_one
- delete.append((media, selector_one))
-
- for item in delete:
- try:
- del self._optimized_css[item[0]][item[1]]
- except:
- #Must have already been deleted
- continue
-
- for media, css in add.iteritems():
- self._optimized_css[media].update(css)
-
-
-
- def __shorthand(self, value):
- """
- Compresses shorthand values. Example: margin:1px 1px 1px 1px . margin:1px
- @value (string)
- """
-
- ##FIX##
-
- important = '';
- if self.parser.is_important(value):
- value_list = self.parser.gvw_important(value)
- important = '!important'
- else:
- value_list = value
-
- ret = value
- value_list = value_list.split(' ')
-
- if len(value_list) == 4:
- if value_list[0] == value_list[1] and value_list[0] == value_list[2] and value_list[0] == value_list[3]:
- ret = value_list[0] + important
-
- elif value_list[1] == value_list[3] and value_list[0] == value_list[2]:
- ret = value_list[0] + ' ' + value_list[1] + important
-
- elif value_list[1] == value_list[3]:
- ret = value_list[0] + ' ' + value_list[1] + ' ' + value_list[2] + important
-
- elif len(value_list) == 3:
- if value_list[0] == value_list[1] and value_list[0] == value_list[2]:
- ret = value_list[0] + important
-
- elif value_list[0] == value_list[2]:
- return value_list[0] + ' ' + value_list[1] + important
-
- elif len(value_list) == 2:
- if value_list[0] == value_list[1]:
- ret = value_list[0] + important
-
- if ret != value:
- self.parser.log('Optimised shorthand notation: Changed "' + value + '" to "' + ret + '"', 'Information')
-
- return ret
-
- def __compress_important(self, value):
- """
- Removes unnecessary whitespace in ! important
- @value (string)
- """
- if self.parser.is_important(value):
- value = self.parser.gvw_important(value) + '!important'
-
- return value
-
- def __compress_numbers(self, prop, value):
- """
- Compresses numbers (ie. 1.0 becomes 1 or 1.100 becomes 1.1 )
- @value (string) is the posible number to be compressed
- """
-
- ##FIX##
-
- value = value.split('/')
-
- for l in xrange(len(value)):
- #continue if no numeric value
- if not (len(value[l]) > 0 and (value[l][0].isdigit() or value[l][0] in ('+', '-') )):
- continue
-
- #Fix bad colors
- if prop in data.color_values:
- value[l] = '#' + value[l]
-
- is_floatable = False
- try:
- float(value[l])
- is_floatable = True
- except:
- pass
-
- if is_floatable and float(value[l]) == 0:
- value[l] = '0'
-
- elif value[l][0] != '#':
- unit_found = False
- for unit in data.units:
- pos = value[l].lower().find(unit)
- if pos != -1 and prop not in data.shorthands:
- value[l] = self.__remove_leading_zeros(float(value[l][:pos])) + unit
- unit_found = True
- break;
-
- if not unit_found and prop in data.unit_values and prop not in data.shorthands:
- value[l] = self.__remove_leading_zeros(float(value[l])) + 'px'
-
- elif not unit_found and prop not in data.shorthands:
- value[l] = self.__remove_leading_zeros(float(value[l]))
-
-
- if len(value) > 1:
- return '/'.join(value)
-
- return value[0]
-
- def __remove_leading_zeros(self, float_val):
- """
- Removes the leading zeros from a float value
- @float_val (float)
- @returns (string)
- """
- #Remove leading zero
- if abs(float_val) < 1:
- if float_val < 0:
- float_val = '-' . str(float_val)[2:]
- else:
- float_val = str(float_val)[1:]
-
- return str(float_val)
-
- def __compress_color(self, color):
- """
- Color compression function. Converts all rgb() values to #-values and uses the short-form if possible. Also replaces 4 color names by #-values.
- @color (string) the {posible} color to change
- """
-
- #rgb(0,0,0) . #000000 (or #000 in this case later)
- if color[:4].lower() == 'rgb(':
- color_tmp = color[4:(len(color)-5)]
- color_tmp = color_tmp.split(',')
-
- for c in color_tmp:
- c = c.strip()
- if c[:-1] == '%':
- c = round((255*color_tmp[i])/100)
-
- if color_tmp[i] > 255:
- color_tmp[i] = 255
-
- color = '#'
-
- for i in xrange(3):
- if color_tmp[i] < 16:
- color += '0' + str(hex(color_tmp[i])).replace('0x', '')
- else:
- color += str(hex(color_tmp[i])).replace('0x', '')
-
- #Fix bad color names
- if data.replace_colors.has_key(color.lower()):
- color = data.replace_colors[color.lower()]
-
- #aabbcc . #abc
- if len(color) == 7:
- color_temp = color.lower()
- if color_temp[0] == '#' and color_temp[1] == color_temp[2] and color_temp[3] == color_temp[4] and color_temp[5] == color_temp[6]:
- color = '#' + color[1] + color[3] + color[5]
-
- if data.optimize_colors.has_key(color.lower()):
- color = data.optimize_colors[color.lower()]
-
- return color
\ No newline at end of file
+++ /dev/null
-# CSSTidy - CSS Printer
-#
-# CSS Printer class
-#
-# This file is part of CSSTidy.
-#
-# CSSTidy is free software you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation either version 2 of the License, or
-# (at your option) any later version.
-#
-# CSSTidy is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with CSSTidy if not, write to the Free Software
-# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-#
-# @license http://opensource.org/licenses/gpl-license.php GNU Public License
-# @package csstidy
-# @author Dj Gilcrease (digitalxero at gmail dot com) 2005-2006
-
-import data
-
-class CSSPrinter(object):
- def __init__(self, parser):
- self.parser = parser
- self._css = {}
- self.__renderMethods = {'string': self.__renderString, 'file': self.__renderFile}
-
-#PUBLIC METHODS
- def prepare(self, css):
- self._css = css
-
- def render(self, output="string", *args, **kwargs):
- return self.__renderMethods[output](*args, **kwargs)
-
-#PRIVATE METHODS
- def __renderString(self, *args, **kwargs):
- ##OPTIMIZE##
- template = self.parser.getSetting('template')
- ret = ""
-
- if template == 'highest_compression':
- top_line_end = ""
- iner_line_end = ""
- bottom_line_end = ""
- indent = ""
-
- elif template == 'high_compression':
- top_line_end = "\n"
- iner_line_end = ""
- bottom_line_end = "\n"
- indent = ""
-
- elif template == 'default':
- top_line_end = "\n"
- iner_line_end = "\n"
- bottom_line_end = "\n\n"
- indent = ""
-
- elif template == 'low_compression':
- top_line_end = "\n"
- iner_line_end = "\n"
- bottom_line_end = "\n\n"
- indent = " "
-
- if self.parser.getSetting('timestamp'):
- ret += '/# CSSTidy ' + self.parser.version + ': ' + datetime.now().strftime("%a, %d %b %Y %H:%M:%S +0000") + ' #/' + top_line_end
-
- for item in self.parser._import:
- ret += '@import(' + item + ');' + top_line_end
-
- for item in self.parser._charset:
- ret += '@charset(' + item + ');' + top_line_end
-
- for item in self.parser._namespace:
- ret += '@namespace(' + item + ');' + top_line_end
-
- for media, css in self._css.iteritems():
- for selector, cssdata in css.iteritems():
- ret += selector + '{' + top_line_end
-
- for item, value in cssdata.iteritems():
- ret += indent + item + ':' + value + ';' + iner_line_end
-
- ret += '}' + bottom_line_end
-
- return ret
-
- def __renderFile(self, filename=None, *args, **kwargs):
- if filename is None:
- return self.__renderString()
-
- try:
- f = open(filename, "w")
- f.write(self.__renderString())
- finally:
- f.close()
\ No newline at end of file
+++ /dev/null
-
-class SortedDict(dict):
- """
- A dictionary that keeps its keys in the order in which they're inserted.
- """
- def __init__(self, data=None):
- if data is None:
- data = {}
- super(SortedDict, self).__init__(data)
- if isinstance(data, dict):
- self.keyOrder = data.keys()
- else:
- self.keyOrder = []
- for key, value in data:
- if key not in self.keyOrder:
- self.keyOrder.append(key)
-
- def __deepcopy__(self, memo):
- from copy import deepcopy
- return self.__class__([(key, deepcopy(value, memo))
- for key, value in self.iteritems()])
-
- def __setitem__(self, key, value):
- super(SortedDict, self).__setitem__(key, value)
- if key not in self.keyOrder:
- self.keyOrder.append(key)
-
- def __delitem__(self, key):
- super(SortedDict, self).__delitem__(key)
- self.keyOrder.remove(key)
-
- def __iter__(self):
- for k in self.keyOrder:
- yield k
-
- def pop(self, k, *args):
- result = super(SortedDict, self).pop(k, *args)
- try:
- self.keyOrder.remove(k)
- except ValueError:
- # Key wasn't in the dictionary in the first place. No problem.
- pass
- return result
-
- def popitem(self):
- result = super(SortedDict, self).popitem()
- self.keyOrder.remove(result[0])
- return result
-
- def items(self):
- return zip(self.keyOrder, self.values())
-
- def iteritems(self):
- for key in self.keyOrder:
- yield key, super(SortedDict, self).__getitem__(key)
-
- def keys(self):
- return self.keyOrder[:]
-
- def iterkeys(self):
- return iter(self.keyOrder)
-
- def values(self):
- return [super(SortedDict, self).__getitem__(k) for k in self.keyOrder]
-
- def itervalues(self):
- for key in self.keyOrder:
- yield super(SortedDict, self).__getitem__(key)
-
- def update(self, dict_):
- for k, v in dict_.items():
- self.__setitem__(k, v)
-
- def setdefault(self, key, default):
- if key not in self.keyOrder:
- self.keyOrder.append(key)
- return super(SortedDict, self).setdefault(key, default)
-
- def value_for_index(self, index):
- """Returns the value of the item at the given zero-based index."""
- return self[self.keyOrder[index]]
-
- def insert(self, index, key, value):
- """Inserts the key, value pair before the item with the given index."""
- if key in self.keyOrder:
- n = self.keyOrder.index(key)
- del self.keyOrder[n]
- if n < index:
- index -= 1
- self.keyOrder.insert(index, key)
- super(SortedDict, self).__setitem__(key, value)
-
- def copy(self):
- """Returns a copy of this object."""
- # This way of initializing the copy means it works for subclasses, too.
- obj = self.__class__(self)
- obj.keyOrder = self.keyOrder[:]
- return obj
-
- def __repr__(self):
- """
- Replaces the normal dict.__repr__ with a version that returns the keys
- in their sorted order.
- """
- return '{%s}' % ', '.join(['%r: %r' % (k, v) for k, v in self.items()])
-
- def clear(self):
- super(SortedDict, self).clear()
- self.keyOrder = []
\ No newline at end of file
from compress.filters.jsmin.jsmin import jsmin
from compress.filter_base import FilterBase
+
class JSMinFilter(FilterBase):
+
def filter_js(self, js):
- return jsmin(js)
\ No newline at end of file
+ return jsmin(js)
from StringIO import StringIO
+
def jsmin(js):
ins = StringIO(js)
outs = StringIO()
str = str[1:]
return str
+
def isAlphanum(c):
"""return true if the character is a letter, digit, underscore,
dollar sign, or non-ASCII character.
"""
- return ((c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or
- (c >= 'A' and c <= 'Z') or c == '_' or c == '$' or c == '\\' or (c is not None and ord(c) > 126));
+ return (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') \
+ or (c >= '0' and c <= '9') or c == '_' or c == '$' or c == '\\' \
+ or (c is not None and ord(c) > 126)
+
class UnterminatedComment(Exception):
pass
+
class UnterminatedStringLiteral(Exception):
pass
+
class UnterminatedRegularExpression(Exception):
pass
+
class JavascriptMinify(object):
def _outA(self):
self.outstream.write(self.theA)
+
def _outB(self):
self.outstream.write(self.theB)
c = self.instream.read(1)
if c >= ' ' or c == '\n':
return c
- if c == '': # EOF
+ if c == '': # EOF
return '\000'
if c == '\r':
return '\n'
self._outA()
self.theA = self._get()
-
if action <= 3:
self.theB = self._next()
if self.theB == '/' and (self.theA == '(' or self.theA == ',' or
self._outA()
self.theB = self._next()
-
def _jsmin(self):
"""Copy the input to the output, deleting the characters which are
insignificant to JavaScript. Comments will be removed. Tabs will be
if __name__ == '__main__':
import sys
jsm = JavascriptMinify()
- jsm.minify(sys.stdin, sys.stdout)
\ No newline at end of file
+ jsm.minify(sys.stdin, sys.stdout)
CSS_ARGUMENTS = getattr(settings, 'COMPRESS_YUI_CSS_ARGUMENTS', '')
JS_ARGUMENTS = getattr(settings, 'COMPRESS_YUI_JS_ARGUMENTS', '')
+
class YUICompressorFilter(FilterBase):
def filter_common(self, content, type_, arguments):
return self.filter_common(js, 'js', JS_ARGUMENTS)
def filter_css(self, css):
- return self.filter_common(css, 'css', CSS_ARGUMENTS)
\ No newline at end of file
+ return self.filter_common(css, 'css', CSS_ARGUMENTS)
from django.conf import settings
+
class Command(NoArgsCommand):
option_list = NoArgsCommand.option_list + (
make_option('--force', action='store_true', default=False, help='Force update of all files, even if the source files are older than the current compressed file.'),
args = ''
def handle_noargs(self, **options):
-
+
force = options.get('force', False)
verbosity = int(options.get('verbosity', 1))
from compress.utils import needs_update, filter_css, filter_js
for name, css in settings.COMPRESS_CSS.items():
- u, version = needs_update(css['output_filename'],
+ u, version = needs_update(css['output_filename'],
css['source_filenames'])
if (force or u) or verbosity >= 2:
print
for name, js in settings.COMPRESS_JS.items():
- u, version = needs_update(js['output_filename'],
+ u, version = needs_update(js['output_filename'],
js['source_filenames'])
if (force or u) or verbosity >= 2:
print
# Backwards compatibility for Django r9110
-if not [opt for opt in Command.option_list if opt.dest=='verbosity']:
+if not [opt for opt in Command.option_list if opt.dest == 'verbosity']:
Command.option_list += (
make_option('--verbosity', '-v', action="store", dest="verbosity",
default='1', type='choice', choices=['0', '1', '2'],
from django import template
-from django.conf import settings as django_settings
-
from compress.conf import settings
from compress.utils import media_root, media_url, needs_update, filter_css, filter_js, get_output_filename, get_version, get_version_from_file
register = template.Library()
+
def render_common(template_name, obj, filename, version):
if settings.COMPRESS:
filename = get_output_filename(filename, version)
context['url'] = filename
else:
context['url'] = media_url(filename, prefix)
-
+
return template.loader.render_to_string(template_name, context)
+
def render_css(css, filename, version=None):
return render_common(css.get('template_name', 'compress/css.html'), css, filename, version)
+
def render_js(js, filename, version=None):
return render_common(js.get('template_name', 'compress/js.html'), js, filename, version)
+
class CompressedCSSNode(template.Node):
+
def __init__(self, name):
self.name = name
try:
css = settings.COMPRESS_CSS[css_name]
except KeyError:
- return '' # fail silently, do not return anything if an invalid group is specified
+ return '' # fail silently, do not return anything if an invalid group is specified
if settings.COMPRESS:
version = None
if settings.COMPRESS_AUTO:
- u, version = needs_update(css['output_filename'],
+ u, version = needs_update(css['output_filename'],
css['source_filenames'])
if u:
filter_css(css)
filename_base, filename = os.path.split(css['output_filename'])
path_name = media_root(filename_base)
version = get_version_from_file(path_name, filename)
-
+
return render_css(css, css['output_filename'], version)
else:
# output source files
return r
+
class CompressedJSNode(template.Node):
+
def __init__(self, name):
self.name = name
try:
js = settings.COMPRESS_JS[js_name]
except KeyError:
- return '' # fail silently, do not return anything if an invalid group is specified
-
+ return '' # fail silently, do not return anything if an invalid group is specified
+
if 'external_urls' in js:
r = ''
for url in js['external_urls']:
r += render_js(js, url)
return r
-
+
if settings.COMPRESS:
version = None
if settings.COMPRESS_AUTO:
- u, version = needs_update(js['output_filename'],
+ u, version = needs_update(js['output_filename'],
js['source_filenames'])
if u:
filter_js(js)
- else:
+ else:
filename_base, filename = os.path.split(js['output_filename'])
path_name = media_root(filename_base)
version = get_version_from_file(path_name, filename)
r += render_js(js, source_file)
return r
+
#@register.tag
def compressed_css(parser, token):
try:
return CompressedCSSNode(name)
compressed_css = register.tag(compressed_css)
+
#@register.tag
def compressed_js(parser, token):
try:
raise template.TemplateSyntaxError, '%r requires exactly one argument: the name of a group in the COMPRESS_JS setting' % token.split_contents()[0]
return CompressedJSNode(name)
+
compressed_js = register.tag(compressed_js)
from compress.conf import settings
from compress.signals import css_filtered, js_filtered
+
def get_class(class_string):
"""
Convert a string version of a function name to the callable object.
return class_string
+
def get_mod_func(callback):
"""
Converts 'django.views.news.stories.story_detail' to
dot = callback.rindex('.')
except ValueError:
return callback, ''
- return callback[:dot], callback[dot+1:]
+ return callback[:dot], callback[dot + 1:]
+
def needs_update(output_file, source_files, verbosity=0):
"""
"""
version = get_version(source_files)
-
+
on = get_output_filename(output_file, version)
compressed_file_full = media_root(on)
if not os.path.exists(compressed_file_full):
return True, version
-
+
update_needed = getattr(get_class(settings.COMPRESS_VERSIONING)(), 'needs_update')(output_file, source_files, version)
return update_needed
+
def media_root(filename):
"""
Return the full path to ``filename``. ``filename`` is a relative path name in MEDIA_ROOT
"""
return os.path.join(django_settings.STATIC_ROOT, filename)
+
def media_url(url, prefix=None):
if prefix:
return prefix + urlquote(url)
return django_settings.STATIC_URL + urlquote(url)
+
def concat(filenames, separator=''):
"""
Concatenate the files from the list of the ``filenames``, ouput separated with ``separator``.
fd.close()
return r
+
def max_mtime(files):
return int(max([os.stat(media_root(f)).st_mtime for f in files]))
+
def save_file(filename, contents):
dirname = os.path.dirname(media_root(filename))
if not os.path.exists(dirname):
fd.write(contents)
fd.close()
+
def get_output_filename(filename, version):
if settings.COMPRESS_VERSION and version is not None:
return filename.replace(settings.COMPRESS_VERSION_PLACEHOLDER, version)
else:
return filename.replace(settings.COMPRESS_VERSION_PLACEHOLDER, settings.COMPRESS_VERSION_DEFAULT)
+
def get_version(source_files, verbosity=0):
version = getattr(get_class(settings.COMPRESS_VERSIONING)(), 'get_version')(source_files)
return version
-
+
+
def get_version_from_file(path, filename):
regex = re.compile(r'^%s$' % (get_output_filename(settings.COMPRESS_VERSION_PLACEHOLDER.join([re.escape(part) for part in filename.split(settings.COMPRESS_VERSION_PLACEHOLDER)]), r'([A-Za-z0-9]+)')))
for f in os.listdir(path):
if result and result.groups():
return result.groups()[0]
-def remove_files(path, filename, verbosity=0):
+
+def remove_files(path, filename, verbosity=0):
regex = re.compile(r'^%s$' % (os.path.basename(get_output_filename(settings.COMPRESS_VERSION_PLACEHOLDER.join([re.escape(part) for part in filename.split(settings.COMPRESS_VERSION_PLACEHOLDER)]), r'[A-Za-z0-9]+'))))
if os.path.exists(path):
for f in os.listdir(path):
if regex.match(f):
if verbosity >= 1:
print "Removing outdated file %s" % f
-
+
os.unlink(os.path.join(path, f))
+
def filter_common(obj, verbosity, filters, attr, separator, signal):
output = concat(obj['source_filenames'], separator)
-
+
filename = get_output_filename(obj['output_filename'], get_version(obj['source_filenames']))
if settings.COMPRESS_VERSION:
save_file(filename, output)
signal.send(None)
+
def filter_css(css, verbosity=0):
return filter_common(css, verbosity, filters=settings.COMPRESS_CSS_FILTERS, attr='filter_css', separator='', signal=css_filtered)
+
def filter_js(js, verbosity=0):
return filter_common(js, verbosity, filters=settings.COMPRESS_JS_FILTERS, attr='filter_js', separator='', signal=js_filtered)
def get_version(self, source_files):
raise NotImplementedError
-
+
def needs_update(self, output_file, source_files, version):
raise NotImplementedError
-
+
+
class VersioningError(Exception):
"""
This exception is raised when version creation fails
"""
- pass
\ No newline at end of file
+ pass
from compress.utils import concat, get_output_filename
from compress.versioning.base import VersioningBase
+
class HashVersioningBase(VersioningBase):
+
def __init__(self, hash_method):
self.hash_method = hash_method
-
+
def needs_update(self, output_file, source_files, version):
output_file_name = get_output_filename(output_file, version)
ph = settings.COMPRESS_VERSION_PLACEHOLDER
of = output_file
try:
phi = of.index(ph)
- old_version = output_file_name[phi:phi+len(ph)-len(of)]
+ old_version = output_file_name[phi:phi + len(ph) - len(of)]
return (version != old_version), version
except ValueError:
# no placeholder found, do not update, manual update if needed
return False, version
-
+
def get_version(self, source_files):
buf = concat(source_files)
s = cStringIO.StringIO(buf)
version = self.get_hash(s)
s.close()
- return version
-
- def get_hash(self, f, CHUNK=2**16):
+ return version
+
+ def get_hash(self, f, CHUNK=2 ** 16):
m = self.hash_method()
while 1:
chunk = f.read(CHUNK)
m.update(chunk)
return m.hexdigest()
+
class MD5Versioning(HashVersioningBase):
+
def __init__(self):
super(MD5Versioning, self).__init__(md5)
+
class SHA1Versioning(HashVersioningBase):
+
def __init__(self):
- super(SHA1Versioning, self).__init__(sha1)
\ No newline at end of file
+ super(SHA1Versioning, self).__init__(sha1)
from compress.utils import get_output_filename, media_root
from compress.versioning.base import VersioningBase
+
class MTimeVersioning(VersioningBase):
def get_version(self, source_files):
compressed_file_full = media_root(output_file_name)
return (int(os.stat(compressed_file_full).st_mtime) < int(version)), version
-
__all__ = ['CASBackend']
+
def _verify_cas1(ticket, service):
"""Verifies CAS 1.0 authentication ticket.
if not username:
return None
try:
- user = User.objects.get(username__iexact = username)
+ user = User.objects.get(username__iexact=username)
except User.DoesNotExist:
# user will have an "unusable" password
user = User.objects.create_user(username, '')
"""Retrieve the user's entry in the User model if it exists"""
try:
- return User.objects.get(pk = user_id)
+ return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
__all__ = ['login_required', 'permission_required', 'user_passes_test']
+
def user_passes_test(test_func, login_url=None,
redirect_field_name=REDIRECT_FIELD_NAME):
"""Replacement for django.contrib.auth.decorators.user_passes_test that
__all__ = ['CASMiddleware']
+
class CASMiddleware(object):
"""Middleware that allows CAS authentication on admin pages"""
from django.db import models
-from django.contrib.auth.models import User
\ No newline at end of file
+from django.contrib.auth.models import User
__all__ = ['login', 'logout']
-def _service_url(request, redirect_to = None):
+
+def _service_url(request, redirect_to=None):
"""Generates application service URL for CAS"""
protocol = ('http://', 'https://')[request.is_secure()]
return urljoin(settings.CAS_SERVER_URL, 'login') + '?' + urlencode(params)
-def _logout_url(request, next_page = None):
+def _logout_url(request, next_page=None):
"""Generates CAS logout URL"""
url = urljoin(settings.CAS_SERVER_URL, 'logout')
return url
-def login(request, next_page = None, required = False):
+def login(request, next_page=None, required=False):
"""Forwards to CAS login URL or verifies CAS ticket"""
print "LOGIN original NEXT_PAGE:", next_page
if request.user.is_authenticated():
message = "You are logged in as %s." % request.user.username
- request.user.message_set.create(message = message)
+ request.user.message_set.create(message=message)
return HttpResponseRedirect(next_page)
ticket = request.GET.get('ticket')
service = _service_url(request, next_page)
print "SERVICE: ", service
if ticket:
from django.contrib import auth
- user = auth.authenticate(ticket = ticket, service = service)
+ user = auth.authenticate(ticket=ticket, service=service)
if user is not None:
auth.login(request, user)
name = user.first_name or user.username
message = "Login succeeded. Welcome, %s." % name
- user.message_set.create(message = message)
+ user.message_set.create(message=message)
return HttpResponseRedirect(next_page)
elif settings.CAS_RETRY_LOGIN or required:
return HttpResponseRedirect(_login_url(service))
return HttpResponseRedirect(_login_url(service))
-def logout(request, next_page = None):
+def logout(request, next_page=None):
"""Redirects to CAS logout page"""
from django.contrib.auth import logout
-# coding: utf-8
+# -*- coding: utf-8
-import locale
-
-import os, re, datetime
-from time import gmtime, strftime
from django.conf import settings
from django.utils.encoding import smart_unicode
+from filebrowser.fb_settings import *
+
+from filebrowser.functions import _get_file_type, _url_join, _is_selectable, \
+ _get_version_path
+
+from time import gmtime, strftime
+
+import locale
+import os
+import re
+import datetime
import urllib
+
# filebrowser imports
-from filebrowser.fb_settings import *
-from filebrowser.functions import _get_file_type, _url_join, _is_selectable, _get_version_path
# PIL import
if STRICT_PIL:
ucode = urllib.quote(ucode)
return ucode
+
class FileObject(object):
"""
The FileObject represents a File on the Server.
-
+
PATH has to be relative to MEDIA_ROOT.
"""
self.path = path
self.head = os.path.split(path)[0]
self.filename = os.path.split(path)[1]
- self.filename_lower = self.filename.lower() # important for sorting
+ self.filename_lower = self.filename.lower() # important for sorting
self.filetype = _get_file_type(self.filename)
def _filesize(self):
def __unicode__(self):
return u"%s" % self.url_save
-
-
-
-# coding: utf-8
+# -*- coding: utf-8
from django.contrib.sessions.models import Session
from django.shortcuts import get_object_or_404, render_to_response
Decorator to recognize a user by its session.
Used for Flash-Uploading.
"""
-
+
def decorator(request, *args, **kwargs):
try:
engine = __import__(settings.SESSION_ENGINE, {}, {}, [''])
request.user = get_object_or_404(User, pk=user_id)
return function(request, *args, **kwargs)
return decorator
-
-
-# coding: utf-8
+# -*- coding: utf-8
import os
from django.conf import settings
'Video': ['.mov', '.wmv', '.mpeg', '.mpg', '.avi', '.rm'],
'Document': ['.pdf', '.doc', '.rtf', '.txt', '.xls', '.csv'],
'Sound': ['.mp3', '.mp4', '.wav', '.aiff', '.midi', '.m4p'],
- 'Code': ['.html', '.py', '.js', '.css']
+ 'Code': ['.html', '.py', '.js', '.css'],
})
# Define different formats for allowed selections.
_('Document')
_('Sound')
_('Code')
-
-
-# coding: utf-8
+# -*- coding: utf-8
import os
from filebrowser.fb_settings import *
from filebrowser.base import FileObject
+
class FileBrowseWidget(Input):
input_type = 'text'
-
+
class Media:
- js = (os.path.join(URL_FILEBROWSER_MEDIA, 'js/AddFileBrowser.js'), )
-
+ js = (os.path.join(URL_FILEBROWSER_MEDIA, 'js/AddFileBrowser.js'),)
+
def __init__(self, attrs=None):
self.directory = attrs.get('directory', '')
self.extensions = attrs.get('extensions', '')
self.attrs = attrs.copy()
else:
self.attrs = {}
-
+
def render(self, name, value, attrs=None):
if value is None:
value = ""
except:
pass
return render_to_string("filebrowser/custom_field.html", locals())
-
+
class FileBrowseFormField(forms.CharField):
widget = FileBrowseWidget
-
+
default_error_messages = {
'extension': _(u'Extension %(ext)s is not allowed. Only %(allowed)s is allowed.'),
}
-
+
def __init__(self, max_length=None, min_length=None,
directory=None, extensions=None, format=None,
*args, **kwargs):
self.format = format or ''
self.extensions = extensions or EXTENSIONS.get(format)
super(FileBrowseFormField, self).__init__(*args, **kwargs)
-
+
def clean(self, value):
value = super(FileBrowseFormField, self).clean(value)
if value == '':
if self.extensions and not file_extension in self.extensions:
raise forms.ValidationError(self.error_messages['extension'] % {'ext': file_extension, 'allowed': ", ".join(self.extensions)})
return value
-
+
class FileBrowseField(Field):
__metaclass__ = models.SubfieldBase
-
+
def __init__(self, *args, **kwargs):
self.directory = kwargs.pop('directory', '')
self.extensions = kwargs.pop('extensions', '')
self.format = kwargs.pop('format', '')
return super(FileBrowseField, self).__init__(*args, **kwargs)
-
+
def to_python(self, value):
if not value or isinstance(value, FileObject):
return value
return FileObject(_url_to_path(value))
-
+
def get_db_prep_value(self, value):
if value is None:
return None
return unicode(value)
-
-
+
def get_manipulator_field_objs(self):
return [oldforms.TextField]
-
+
def get_internal_type(self):
return "CharField"
-
+
def formfield(self, **kwargs):
attrs = {}
attrs["directory"] = self.directory
'widget': FileBrowseWidget(attrs=attrs),
'directory': self.directory,
'extensions': self.extensions,
- 'format': self.format
+ 'format': self.format,
}
defaults.update(kwargs)
return super(FileBrowseField, self).formfield(**defaults)
-
-
# coding: utf-8
-import re, os
+import os
+import re
from django import forms
from django.forms.formsets import BaseFormSet
alnum_name_re = re.compile(r'^[\sa-zA-Z0-9._/-]+$')
+
class MakeDirForm(forms.Form):
"""
Form for creating Directory.
"""
-
+
def __init__(self, path, *args, **kwargs):
self.path = path
super(MakeDirForm, self).__init__(*args, **kwargs)
-
- dir_name = forms.CharField(widget=forms.TextInput(attrs=dict({ 'class': 'vTextField' }, max_length=50, min_length=3)), label=_(u'Name'), help_text=_(u'Only letters, numbers, underscores, spaces and hyphens are allowed.'), required=True)
-
- def clean_dir_name(self):
+
+ dir_name = forms.CharField(widget=forms.TextInput(attrs=dict({'class': 'vTextField'}, max_length=50, min_length=3)), label=_(u'Name'), help_text=_(u'Only letters, numbers, underscores, spaces and hyphens are allowed.'), required=True)
+
+ def clean_dir_name(self):
if self.cleaned_data['dir_name']:
# only letters, numbers, underscores, spaces and hyphens are allowed.
if not alnum_name_re.search(self.cleaned_data['dir_name']):
if os.path.isdir(os.path.join(self.path, _convert_filename(self.cleaned_data['dir_name']))):
raise forms.ValidationError(_(u'The Folder already exists.'))
return _convert_filename(self.cleaned_data['dir_name'])
-
+
class RenameForm(forms.Form):
"""
Form for renaming File/Directory.
"""
-
+
def __init__(self, path, file_extension, *args, **kwargs):
self.path = path
self.file_extension = file_extension
super(RenameForm, self).__init__(*args, **kwargs)
-
- name = forms.CharField(widget=forms.TextInput(attrs=dict({ 'class': 'vTextField' }, max_length=50, min_length=3)), label=_(u'New Name'), help_text=_('Only letters, numbers, underscores, spaces and hyphens are allowed.'), required=True)
-
+
+ name = forms.CharField(widget=forms.TextInput(attrs=dict({'class': 'vTextField'}, max_length=50, min_length=3)), label=_(u'New Name'), help_text=_('Only letters, numbers, underscores, spaces and hyphens are allowed.'), required=True)
+
def clean_name(self):
if self.cleaned_data['name']:
# only letters, numbers, underscores, spaces and hyphens are allowed.
if os.path.isdir(os.path.join(self.path, _convert_filename(self.cleaned_data['name']))) or os.path.isfile(os.path.join(self.path, _convert_filename(self.cleaned_data['name']) + self.file_extension)):
raise forms.ValidationError(_(u'The File/Folder already exists.'))
return _convert_filename(self.cleaned_data['name'])
-
-
-# coding: utf-8
+# -*- coding: utf-8
+
+import os
+import re
+import decimal
from django.utils.translation import ugettext as _
from django.utils.safestring import mark_safe
from time import gmtime, strftime, localtime, mktime, time
from django.core.files import File
from django.core.files.storage import default_storage
-import os, re, decimal
+
from urlparse import urlparse
# filebrowser imports
"""
Change URL to PATH.
Value has to be an URL relative to MEDIA URL or a full URL (including MEDIA_URL).
-
+
Returns a PATH relative to MEDIA_ROOT.
"""
print "URL2PATH", repr(value)
"""
Change PATH to URL.
Value has to be a PATH relative to MEDIA_ROOT.
-
+
Return an URL relative to MEDIA_ROOT.
"""
mediaroot_re = re.compile(r'^(%s)' % (MEDIA_ROOT))
"""
Construct the PATH to an Image version.
Value has to be server-path, relative to MEDIA_ROOT.
-
+
version_filename = filename + version_prefix + ext
Returns a path relative to MEDIA_ROOT.
"""
Get filterdate.
"""
- returnvalue = ''
dateYear = strftime("%Y", gmtime(dateTime))
dateMonth = strftime("%m", gmtime(dateTime))
dateDay = strftime("%d", gmtime(dateTime))
- if filterDate == 'today' and int(dateYear) == int(localtime()[0]) and int(dateMonth) == int(localtime()[1]) and int(dateDay) == int(localtime()[2]): returnvalue = 'true'
- elif filterDate == 'thismonth' and dateTime >= time() - 2592000: returnvalue = 'true'
- elif filterDate == 'thisyear' and int(dateYear) == int(localtime()[0]): returnvalue = 'true'
- elif filterDate == 'past7days' and dateTime >= time() - 604800: returnvalue = 'true'
- elif filterDate == '': returnvalue = 'true'
- return returnvalue
+
+ if filterDate == 'today' and int(dateYear) == int(localtime()[0]) and int(dateMonth) == int(localtime()[1]) and int(dateDay) == int(localtime()[2]):
+ return 'true'
+
+ if filterDate == 'thismonth' and dateTime >= time() - 2592000:
+ return 'true'
+
+ if filterDate == 'thisyear' and int(dateYear) == int(localtime()[0]):
+ return 'true'
+
+ if filterDate == 'past7days' and dateTime >= time() - 604800:
+ return 'true'
+
+ return 'true' if filterDate == '' else ''
def _get_settings_var():
return select_types
-def _version_generator(value, version_prefix, force = None):
+def _version_generator(value, version_prefix, force=None):
"""
Generate Version for an Image.
value has to be a serverpath relative to MEDIA_ROOT.
from PIL import ImageFile
except ImportError:
import ImageFile
- ImageFile.MAXBLOCK = IMAGE_MAXBLOCK # default is 64k
+ ImageFile.MAXBLOCK = IMAGE_MAXBLOCK # default is 64k
try:
im = Image.open(os.path.join(MEDIA_ROOT, value))
os.chmod(version_dir, 0775)
version = scale_and_crop(im, VERSIONS[version_prefix]['width'], VERSIONS[version_prefix]['height'], VERSIONS[version_prefix]['opts'])
try:
- version.save(absolute_version_path, quality = 90, optimize = 1)
+ version.save(absolute_version_path, quality=90, optimize=1)
except IOError:
- version.save(absolute_version_path, quality = 90)
+ version.save(absolute_version_path, quality=90)
return version_path
except:
return None
r = min(xr / x, yr / y)
if r < 1.0 or (r > 1.0 and 'upscale' in opts):
- im = im.resize((int(x * r), int(y * r)), resample = Image.ANTIALIAS)
+ im = im.resize((int(x * r), int(y * r)), resample=Image.ANTIALIAS)
if 'crop' in opts:
x, y = [float(v) for v in im.size]
return value.replace(" ", "_").lower()
else:
return value
-
-
-
-# This file is only necessary for the tests to work
\ No newline at end of file
+# This file is only necessary for the tests to work
register = template.Library()
-@register.inclusion_tag('filebrowser/include/_response.html', takes_context = True)
-def query_string(context, add = None, remove = None):
+
+@register.inclusion_tag('filebrowser/include/_response.html', takes_context=True)
+def query_string(context, add=None, remove=None):
"""
Allows the addition and removal of query string parameters.
-
+
_response.html is just {{ response }}
-
+
Usage:
http://www.url.com/{% query_string "param_to_add=value, param_to_add=value" "param_to_remove, params_to_remove" %}
http://www.url.com/{% query_string "" "filter" %}filter={{new_filter}}
remove = string_to_list(remove)
params = context['query'].copy()
response = get_query_string(params, add, remove)
- return {'response': smart_unicode(response) }
+ return {'response': smart_unicode(response)}
-def query_helper(query, add = None, remove = None):
+def query_helper(query, add=None, remove=None):
"""
Helper Function for use within views.
"""
return get_query_string(params, add, remove)
-def get_query_string(p, new_params = None, remove = None):
+def get_query_string(p, new_params=None, remove=None):
"""
Add and remove query parameters. From `django.contrib.admin`.
"""
- if new_params is None: new_params = {}
- if remove is None: remove = []
+ if new_params is None:
+ new_params = {}
+
+ if remove is None:
+ remove = []
+
for r in remove:
for k in p.keys():
if k.startswith(r):
def string_to_dict(string):
"""
Usage::
-
+
{{ url|thumbnail:"width=10,height=20" }}
{{ url|thumbnail:"width=10" }}
{{ url|thumbnail:"height=20" }}
"""
kwargs = {}
+
if string:
string = str(string)
- if ',' not in string:
- # ensure at least one ','
- string += ','
- for arg in string.split(','):
- arg = arg.strip()
- if arg == '': continue
- kw, val = arg.split('=', 1)
- kwargs[kw] = val
+
+ args = (arg.strip() for arg in string.split(',') if not arg.iswhitespace())
+ kwargs.update(arg.split('=', 1) for arg in args)
+
return kwargs
def string_to_list(string):
"""
Usage::
-
+
{{ url|thumbnail:"width,height" }}
"""
args = []
string += ','
for arg in string.split(','):
arg = arg.strip()
- if arg == '': continue
+ if arg == '':
+ continue
args.append(arg)
return args
try:
tag, filetype, format = token.split_contents()
except:
- raise TemplateSyntaxError, "%s tag requires 2 arguments" % token.contents.split()[0]
+ raise TemplateSyntaxError("%s tag requires 2 arguments" % token.contents.split()[0])
return SelectableNode(filetype, format)
# coding: utf-8
-import os, re
-from time import gmtime
+import os
+import re
from django.template import Library, Node, Variable, VariableDoesNotExist, TemplateSyntaxError
from django.conf import settings
from django.utils.encoding import force_unicode
-import urllib
# filebrowser imports
from filebrowser.fb_settings import MEDIA_ROOT, MEDIA_URL, VERSIONS
from filebrowser.functions import _url_to_path, _path_to_url, _get_version_path, _version_generator
from filebrowser.base import FileObject
+import logging
+logger = logging.getLogger("django.filebrowser")
+
register = Library()
+
class VersionNode(Node):
def __init__(self, src, version_prefix):
self.src = Variable(src)
version_path = _version_generator(_url_to_path(str(source)), version_prefix)
elif os.path.getmtime(os.path.join(MEDIA_ROOT, _url_to_path(str(source)))) > os.path.getmtime(os.path.join(MEDIA_ROOT, version_path)):
# recreate version if original image was updated
- version_path = _version_generator(_url_to_path(str(source)), version_prefix, force = True)
+ version_path = _version_generator(_url_to_path(str(source)), version_prefix, force=True)
return _path_to_url(version_path)
- except Exception, e:
- import traceback, sys
- print "FB VERSION ERROR"
- traceback.print_exc(file = sys.stdout)
+ except Exception:
+ logger.exception("Version error")
return u""
-
def version(parser, token):
"""
Displaying a version of an existing Image according to the predefined VERSIONS settings (see fb_settings).
{% version field_name version_prefix %}
-
+
Use {% version my_image 'medium' %} in order to display the medium-size
version of an Image stored in a field name my_image.
-
+
version_prefix can be a string or a variable. if version_prefix is a string, use quotes.
"""
try:
tag, src, version_prefix = token.split_contents()
except:
- raise TemplateSyntaxError, "%s tag requires 2 arguments" % token.contents.split()[0]
+ raise TemplateSyntaxError("%s tag requires 2 arguments" % token.contents.split()[0])
if (version_prefix[0] == version_prefix[-1] and version_prefix[0] in ('"', "'")) and version_prefix.lower()[1:-1] not in VERSIONS:
- raise TemplateSyntaxError, "%s tag received bad version_prefix %s" % (tag, version_prefix)
+ raise TemplateSyntaxError("%s tag received bad version_prefix %s" % (tag, version_prefix))
return VersionNode(src, version_prefix)
class VersionObjectNode(Node):
+
def __init__(self, src, version_prefix, var_name):
self.var_name = var_name
self.src = Variable(src)
version_path = _version_generator(_url_to_path(str(source)), version_prefix)
elif os.path.getmtime(os.path.join(MEDIA_ROOT, _url_to_path(str(source)))) > os.path.getmtime(os.path.join(MEDIA_ROOT, version_path)):
# recreate version if original image was updated
- version_path = _version_generator(_url_to_path(str(source)), version_prefix, force = True)
+ version_path = _version_generator(_url_to_path(str(source)), version_prefix, force=True)
context[self.var_name] = FileObject(version_path)
except:
context[self.var_name] = ""
"""
Returns a context variable 'version_object'.
{% version_object field_name version_prefix %}
-
+
Use {% version_object my_image 'medium' %} in order to retrieve the medium
version of an Image stored in a field name my_image.
Use {% version_object my_image 'medium' as var %} in order to use 'var' as
your context variable.
-
+
version_prefix can be a string or a variable. if version_prefix is a string, use quotes.
"""
#tag, src, version_prefix = token.split_contents()
tag, arg = token.contents.split(None, 1)
except:
- raise TemplateSyntaxError, "%s tag requires arguments" % token.contents.split()[0]
+ raise TemplateSyntaxError("%s tag requires arguments" % token.contents.split()[0])
m = re.search(r'(.*?) (.*?) as (\w+)', arg)
if not m:
- raise TemplateSyntaxError, "%r tag had invalid arguments" % tag
+ raise TemplateSyntaxError("%r tag had invalid arguments" % tag)
src, version_prefix, var_name = m.groups()
if (version_prefix[0] == version_prefix[-1] and version_prefix[0] in ('"', "'")) and version_prefix.lower()[1:-1] not in VERSIONS:
- raise TemplateSyntaxError, "%s tag received bad version_prefix %s" % (tag, version_prefix)
+ raise TemplateSyntaxError("%s tag received bad version_prefix %s" % (tag, version_prefix))
return VersionObjectNode(src, version_prefix, var_name)
class VersionSettingNode(Node):
+
def __init__(self, version_prefix):
if (version_prefix[0] == version_prefix[-1] and version_prefix[0] in ('"', "'")):
self.version_prefix = version_prefix[1:-1]
try:
tag, version_prefix = token.split_contents()
except:
- raise TemplateSyntaxError, "%s tag requires 1 argument" % token.contents.split()[0]
+ raise TemplateSyntaxError("%s tag requires 1 argument" % token.contents.split()[0])
if (version_prefix[0] == version_prefix[-1] and version_prefix[0] in ('"', "'")) and version_prefix.lower()[1:-1] not in VERSIONS:
- raise TemplateSyntaxError, "%s tag received bad version_prefix %s" % (tag, version_prefix)
+ raise TemplateSyntaxError("%s tag received bad version_prefix %s" % (tag, version_prefix))
return VersionSettingNode(version_prefix)
register.tag(version)
register.tag(version_object)
register.tag(version_setting)
-
-
from django.conf.urls.defaults import *
urlpatterns = patterns('',
-
+
# filebrowser urls
url(r'^browse/$', 'filebrowser.views.browse', name="fb_browse"),
url(r'^mkdir/', 'filebrowser.views.mkdir', name="fb_mkdir"),
url(r'^rename/$', 'filebrowser.views.rename', name="fb_rename"),
url(r'^delete/$', 'filebrowser.views.delete', name="fb_delete"),
url(r'^versions/$', 'filebrowser.views.versions', name="fb_versions"),
-
+
url(r'^check_file/$', 'filebrowser.views._check_file', name="fb_check"),
url(r'^upload_file/$', 'filebrowser.views._upload_file', name="fb_do_upload"),
# coding: utf-8
# general imports
-import os, re, locale
-from time import gmtime, strftime
+import re
# django imports
from django.shortcuts import render_to_response, HttpResponse
# filebrowser imports
from filebrowser.fb_settings import *
-from filebrowser.functions import _url_to_path, _path_to_url, _get_path, _get_file, _get_version_path, _get_breadcrumbs, _get_filterdate, _get_settings_var, _handle_file_upload, _get_file_type, _url_join, _convert_filename
+
+from filebrowser.functions import (
+ _url_to_path, _path_to_url, _get_path, _get_file, _get_version_path,
+ _get_breadcrumbs, _get_filterdate, _get_settings_var, _handle_file_upload,
+ _get_file_type, _url_join, _convert_filename)
+
from filebrowser.templatetags.fb_tags import query_helper
from filebrowser.base import FileObject
from filebrowser.decorators import flash_login_required
# Precompile regular expressions
filter_re = []
+
for exp in EXCLUDE:
- filter_re.append(re.compile(exp))
+ filter_re.append(re.compile(exp))
for k, v in VERSIONS.iteritems():
exp = (r'_%s.(%s)') % (k, '|'.join(EXTENSION_LIST))
filter_re.append(re.compile(exp))
if path is None:
msg = _('Directory/File does not exist.')
- request.user.message_set.create(message = msg)
+ request.user.message_set.create(message=msg)
if directory is None:
# The DIRECTORY does not exist, raise an error to prevent eternal redirecting.
- raise ImproperlyConfigured, _("Error finding upload directory. Maybe it does not exist?")
+ raise ImproperlyConfigured(_("Error finding upload directory. Maybe it does not exist?"))
redirect_url = reverse("fb_browse") + query_helper(query, "", "dir")
return HttpResponseRedirect(redirect_url)
abs_path = os.path.join(MEDIA_ROOT, DIRECTORY, path)
# INITIAL VARIABLES
- results_var = {'results_total': 0, 'results_current': 0, 'delete_total': 0, 'images_total': 0, 'select_total': 0 }
+ results_var = {'results_total': 0, 'results_current': 0, 'delete_total': 0, 'images_total': 0, 'select_total': 0}
counter = {}
for k, v in EXTENSIONS.iteritems():
counter[k] = 0
counter[fileobject.filetype] += 1
# SORTING
- files.sort(key = lambda e: getattr(e, request.GET.get('o', DEFAULT_ORDER)))
+ files.sort(key=lambda e: getattr(e, request.GET.get('o', DEFAULT_ORDER)))
if request.GET.get('ot') == "desc":
files.reverse()
'title': _(u'FileBrowser'),
'settings_var': _get_settings_var(),
'breadcrumbs': _get_breadcrumbs(query, path, ''),
- }, context_instance = Context(request))
+ }, context_instance=Context(request))
browse = staff_member_required(never_cache(browse))
# mkdir signals
-filebrowser_pre_createdir = Signal(providing_args = ["path", "dirname"])
-filebrowser_post_createdir = Signal(providing_args = ["path", "dirname"])
+filebrowser_pre_createdir = Signal(providing_args=["path", "dirname"])
+filebrowser_post_createdir = Signal(providing_args=["path", "dirname"])
+
def mkdir(request):
"""
path = _get_path(query.get('dir', ''))
if path is None:
msg = _('Directory/File does not exist.')
- request.user.message_set.create(message = msg)
+ request.user.message_set.create(message=msg)
return HttpResponseRedirect(reverse("fb_browse"))
abs_path = os.path.join(MEDIA_ROOT, DIRECTORY, path)
server_path = os.path.join(abs_path, form.cleaned_data['dir_name'])
try:
# PRE CREATE SIGNAL
- filebrowser_pre_createdir.send(sender = request, path = path, dirname = form.cleaned_data['dir_name'])
+ filebrowser_pre_createdir.send(sender=request, path=path, dirname=form.cleaned_data['dir_name'])
# CREATE FOLDER
os.mkdir(server_path)
os.chmod(server_path, 0775)
# POST CREATE SIGNAL
- filebrowser_post_createdir.send(sender = request, path = path, dirname = form.cleaned_data['dir_name'])
+ filebrowser_post_createdir.send(sender=request, path=path, dirname=form.cleaned_data['dir_name'])
# MESSAGE & REDIRECT
msg = _('The Folder %s was successfully created.') % (form.cleaned_data['dir_name'])
- request.user.message_set.create(message = msg)
+ request.user.message_set.create(message=msg)
# on redirect, sort by date desc to see the new directory on top of the list
# remove filter in order to actually _see_ the new folder
redirect_url = reverse("fb_browse") + query_helper(query, "ot=desc,o=date", "ot,o,filter_type,filter_date,q")
'title': _(u'New Folder'),
'settings_var': _get_settings_var(),
'breadcrumbs': _get_breadcrumbs(query, path, _(u'New Folder')),
- }, context_instance = Context(request))
+ }, context_instance=Context(request))
mkdir = staff_member_required(never_cache(mkdir))
path = _get_path(query.get('dir', ''))
if path is None:
msg = _('Directory/File does not exist.')
- request.user.message_set.create(message = msg)
+ request.user.message_set.create(message=msg)
return HttpResponseRedirect(reverse("fb_browse"))
abs_path = os.path.join(MEDIA_ROOT, DIRECTORY, path)
'settings_var': _get_settings_var(),
'breadcrumbs': _get_breadcrumbs(query, path, _(u'Upload')),
'session_key': session_key,
- }, context_instance = Context(request))
+ }, context_instance=Context(request))
upload = staff_member_required(never_cache(upload))
# upload signals
-filebrowser_pre_upload = Signal(providing_args = ["path", "file"])
-filebrowser_post_upload = Signal(providing_args = ["path", "file"])
+filebrowser_pre_upload = Signal(providing_args=["path", "file"])
+filebrowser_post_upload = Signal(providing_args=["path", "file"])
+
def _upload_file(request):
"""
filedata = request.FILES['Filedata']
filedata.name = _convert_filename(filedata.name)
# PRE UPLOAD SIGNAL
- filebrowser_pre_upload.send(sender = request, path = request.POST.get('folder'), file = filedata)
+ filebrowser_pre_upload.send(sender=request, path=request.POST.get('folder'), file=filedata)
# HANDLE UPLOAD
uploadedfile = _handle_file_upload(abs_path, filedata)
# MOVE UPLOADED FILE
new_file = os.path.join(abs_path, uploadedfile)
file_move_safe(new_file, old_file)
# POST UPLOAD SIGNAL
- filebrowser_post_upload.send(sender = request, path = request.POST.get('folder'), file = FileObject(os.path.join(DIRECTORY, folder, filedata.name)))
+ filebrowser_post_upload.send(sender=request, path=request.POST.get('folder'), file=FileObject(os.path.join(DIRECTORY, folder, filedata.name)))
return HttpResponse('True')
_upload_file = flash_login_required(_upload_file)
# delete signals
-filebrowser_pre_delete = Signal(providing_args = ["path", "filename"])
-filebrowser_post_delete = Signal(providing_args = ["path", "filename"])
+filebrowser_pre_delete = Signal(providing_args=["path", "filename"])
+filebrowser_post_delete = Signal(providing_args=["path", "filename"])
+
def delete(request):
"""
Delete existing File/Directory.
-
+
When trying to delete a Directory, the Directory has to be empty.
"""
filename = _get_file(query.get('dir', ''), query.get('filename', ''))
if path is None or filename is None:
msg = _('Directory/File does not exist.')
- request.user.message_set.create(message = msg)
+ request.user.message_set.create(message=msg)
return HttpResponseRedirect(reverse("fb_browse"))
abs_path = os.path.join(MEDIA_ROOT, DIRECTORY, path)
relative_server_path = os.path.join(DIRECTORY, path, filename)
try:
# PRE DELETE SIGNAL
- filebrowser_pre_delete.send(sender = request, path = path, filename = filename)
+ filebrowser_pre_delete.send(sender=request, path=path, filename=filename)
# DELETE IMAGE VERSIONS/THUMBNAILS
for version in VERSIONS:
try:
# DELETE FILE
os.unlink(os.path.join(abs_path, filename))
# POST DELETE SIGNAL
- filebrowser_post_delete.send(sender = request, path = path, filename = filename)
+ filebrowser_post_delete.send(sender=request, path=path, filename=filename)
# MESSAGE & REDIRECT
msg = _('The file %s was successfully deleted.') % (filename.lower())
- request.user.message_set.create(message = msg)
+ request.user.message_set.create(message=msg)
redirect_url = reverse("fb_browse") + query_helper(query, "", "filename,filetype")
return HttpResponseRedirect(redirect_url)
except OSError:
else:
try:
# PRE DELETE SIGNAL
- filebrowser_pre_delete.send(sender = request, path = path, filename = filename)
+ filebrowser_pre_delete.send(sender=request, path=path, filename=filename)
# DELETE FOLDER
os.rmdir(os.path.join(abs_path, filename))
# POST DELETE SIGNAL
- filebrowser_post_delete.send(sender = request, path = path, filename = filename)
+ filebrowser_post_delete.send(sender=request, path=path, filename=filename)
# MESSAGE & REDIRECT
msg = _('The directory %s was successfully deleted.') % (filename.lower())
- request.user.message_set.create(message = msg)
+ request.user.message_set.create(message=msg)
redirect_url = reverse("fb_browse") + query_helper(query, "", "filename,filetype")
return HttpResponseRedirect(redirect_url)
except OSError:
msg = OSError
if msg:
- request.user.message_set.create(message = msg)
+ request.user.message_set.create(message=msg)
return render_to_response('filebrowser/index.html', {
'dir': dir_name,
'query': query,
'settings_var': _get_settings_var(),
'breadcrumbs': _get_breadcrumbs(query, dir_name, ''),
- }, context_instance = Context(request))
+ }, context_instance=Context(request))
delete = staff_member_required(never_cache(delete))
# delete signals
-filebrowser_pre_rename = Signal(providing_args = ["path", "filename"])
-filebrowser_post_rename = Signal(providing_args = ["path", "filename"])
+filebrowser_pre_rename = Signal(providing_args=["path", "filename"])
+filebrowser_post_rename = Signal(providing_args=["path", "filename"])
+
def rename(request):
"""
Rename existing File/Directory.
-
+
Includes renaming existing Image Versions/Thumbnails.
"""
filename = _get_file(query.get('dir', ''), query.get('filename', ''))
if path is None or filename is None:
msg = _('Directory/File does not exist.')
- request.user.message_set.create(message = msg)
+ request.user.message_set.create(message=msg)
return HttpResponseRedirect(reverse("fb_browse"))
abs_path = os.path.join(MEDIA_ROOT, DIRECTORY, path)
file_extension = os.path.splitext(filename)[1].lower()
new_relative_server_path = os.path.join(DIRECTORY, path, form.cleaned_data['name'] + file_extension)
try:
# PRE RENAME SIGNAL
- filebrowser_pre_delete.send(sender = request, path = path, filename = filename)
+ filebrowser_pre_delete.send(sender=request, path=path, filename=filename)
# DELETE IMAGE VERSIONS/THUMBNAILS
# regenerating versions/thumbs will be done automatically
for version in VERSIONS:
# RENAME ORIGINAL
os.rename(os.path.join(MEDIA_ROOT, relative_server_path), os.path.join(MEDIA_ROOT, new_relative_server_path))
# POST RENAME SIGNAL
- filebrowser_post_delete.send(sender = request, path = path, filename = filename)
+ filebrowser_post_delete.send(sender=request, path=path, filename=filename)
# MESSAGE & REDIRECT
msg = _('Renaming was successful.')
- request.user.message_set.create(message = msg)
+ request.user.message_set.create(message=msg)
redirect_url = reverse("fb_browse") + query_helper(query, "", "filename")
return HttpResponseRedirect(redirect_url)
except OSError, (errno, strerror):
'title': _(u'Rename "%s"') % filename,
'settings_var': _get_settings_var(),
'breadcrumbs': _get_breadcrumbs(query, path, _(u'Rename')),
- }, context_instance = Context(request))
+ }, context_instance=Context(request))
rename = staff_member_required(never_cache(rename))
filename = _get_file(query.get('dir', ''), query.get('filename', ''))
if path is None or filename is None:
msg = _('Directory/File does not exist.')
- request.user.message_set.create(message = msg)
+ request.user.message_set.create(message=msg)
return HttpResponseRedirect(reverse("fb_browse"))
abs_path = os.path.join(MEDIA_ROOT, DIRECTORY, path)
'title': _(u'Versions for "%s"') % filename,
'settings_var': _get_settings_var(),
'breadcrumbs': _get_breadcrumbs(query, path, _(u'Versions for "%s"') % filename),
- }, context_instance = Context(request))
-versions = staff_member_required(never_cache(versions))
-
+ }, context_instance=Context(request))
+versions = staff_member_required(never_cache(versions))
from toolbar import models
+
class ButtonAdminForm(forms.ModelForm):
class Meta:
model = models.Button
except ValueError, e:
raise forms.ValidationError(e)
+
class ButtonAdmin(admin.ModelAdmin):
form = ButtonAdminForm
list_display = ('slug', 'label', 'tooltip', 'accesskey')
admin.site.register(models.Button, ButtonAdmin)
admin.site.register(models.ButtonGroup)
-admin.site.register(models.Scriptlet)
\ No newline at end of file
+admin.site.register(models.Scriptlet)
-# To change this template, choose Tools | Templates
-# and open the template in the editor.
-
-
+#
# -*- coding: utf-8 -*-
#
# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
-__author__="lreqc"
-__date__ ="$2009-09-08 14:31:26$"
+
from django.core.management.base import NoArgsCommand
from toolbar.models import Button, ButtonGroup
from django.utils import simplejson as json
import re
+
class Command(NoArgsCommand):
-
+
def handle_noargs(self, **options):
buttons = Button.objects.all()
print "Validating parameters... "
for b in buttons:
- params = b.params;
+ params = b.params
try:
- v = json.loads(b.params)
+ v = json.loads(b.params)
except ValueError, e:
print 'Trying to fix button "%s" ...' % b.slug
if params[0] == u'(':
if b.slug not in hash:
hash[b.slug] = b
continue
-
+
# duplicate button
print "Found duplicate of '%s'" % b.slug
a = hash[b.slug]
print "Searching for empty groups and orphaned buttons:"
for g in ButtonGroup.objects.all():
if len(g.button_set.all()) == 0:
- print "Empty group: '%s'" % g.slug
-
+ print "Empty group: '%s'" % g.slug
+
for b in Button.objects.all():
if len(b.group.all()) == 0:
- print "orphan: '%s'" % b.slug
+ print "orphan: '%s'" % b.slug
from django.db import models
class Migration(SchemaMigration):
-
+
def forwards(self, orm):
-
+
# Adding model 'ButtonGroup'
db.create_table('toolbar_buttongroup', (
('position', self.gf('django.db.models.fields.IntegerField')(default=0)),
('name', self.gf('django.db.models.fields.CharField')(max_length=64, primary_key=True)),
))
db.send_create_signal('toolbar', ['Scriptlet'])
-
-
+
+
def backwards(self, orm):
-
+
# Deleting model 'ButtonGroup'
db.delete_table('toolbar_buttongroup')
# Deleting model 'Scriptlet'
db.delete_table('toolbar_scriptlet')
-
-
+
+
models = {
'toolbar.button': {
'Meta': {'object_name': 'Button'},
'name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'primary_key': 'True'})
}
}
-
+
complete_apps = ['toolbar']
from django.db import models
class Migration(SchemaMigration):
-
+
def forwards(self, orm):
-
+
# Deleting field 'Button.key_mod'
db.delete_column('toolbar_button', 'key_mod')
# Changing field 'Button.key'
db.alter_column('toolbar_button', 'key', self.gf('django.db.models.fields.CharField')(max_length=1, null=True))
-
+
def backwards(self, orm):
-
+
# Adding field 'Button.key_mod'
db.add_column('toolbar_button', 'key_mod', self.gf('django.db.models.fields.PositiveIntegerField')(default=1, blank=True), keep_default=False)
# Changing field 'Button.key'
- db.alter_column('toolbar_button', 'key', self.gf('django.db.models.fields.CharField')(max_length=1, blank=True))
-
+ db.alter_column('toolbar_button', 'key', self.gf('django.db.models.fields.CharField')(max_length=1, blank=True))
+
models = {
'toolbar.button': {
'Meta': {'object_name': 'Button'},
'name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'primary_key': 'True'})
}
}
-
+
complete_apps = ['toolbar']
from django.db import models
class Migration(SchemaMigration):
-
+
def forwards(self, orm):
-
+
# Deleting field 'Button.key'
db.rename_column('toolbar_button', 'key', 'accesskey')
-
-
- def backwards(self, orm):
+
+
+ def backwards(self, orm):
db.rename_column('toolbar_button', 'accesskey', 'key')
-
-
+
+
models = {
'toolbar.button': {
'Meta': {'object_name': 'Button'},
'name': ('django.db.models.fields.CharField', [], {'max_length': '64', 'primary_key': 'True'})
}
}
-
+
complete_apps = ['toolbar']
# -*- coding: utf-8 -*-
#
# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
from django.db import models
from django.utils.translation import ugettext_lazy as _
class ButtonGroup(models.Model):
- name = models.CharField(max_length = 32)
+ name = models.CharField(max_length=32)
slug = models.SlugField()
- position = models.IntegerField(default = 0)
+ position = models.IntegerField(default=0)
class Meta:
ordering = ('position', 'name',)
def __unicode__(self):
return self.name
- def to_dict(self, with_buttons = False):
+ def to_dict(self, with_buttons=False):
d = {'name': self.name, 'position': self.position}
if with_buttons:
- d['buttons'] = [ b.to_dict() for b in self.button_set.all() ]
+ d['buttons'] = [b.to_dict() for b in self.button_set.all()]
return d
-#class ButtonGroupManager(models.Manager):
-#
-# def with_buttons(self):
-# from django.db import connection
-# cursor = connection.cursor()
-# cursor.execute("""
-# SELECT g.name, g.slug, CONCAT(b.slug),
-# FROM toolbar_buttongroup as g LEFT JOIN toolbar_button as b
-#
-# WHERE p.id = r.poll_id
-# GROUP BY 1, 2, 3
-# ORDER BY 3 DESC""")
-# result_list = []
-# for row in cursor.fetchall():
-# p = self.model(id=row[0], question=row[1], poll_date=row[2])
-# p.num_responses = row[3]
-# result_list.append(p)
-# return result_list
class Button(models.Model):
- label = models.CharField(max_length = 32)
- slug = models.SlugField(unique = True) #unused
+ label = models.CharField(max_length=32)
+ slug = models.SlugField(unique=True) # unused
# behaviour
- params = models.TextField(default = '[]') # TODO: should be a JSON field
- scriptlet = models.ForeignKey('Scriptlet', null = True, blank = True)
- link = models.CharField(max_length = 256, blank = True, default = '')
+ params = models.TextField(default='[]') # TODO: should be a JSON field
+ scriptlet = models.ForeignKey('Scriptlet', null=True, blank=True)
+ link = models.CharField(max_length=256, blank=True, default='')
# ui related stuff
- accesskey = models.CharField(null = True, max_length = 1)
-
- tooltip = models.CharField(blank = True, max_length = 120)
+ accesskey = models.CharField(null=True, max_length=1)
+
+ tooltip = models.CharField(blank=True, max_length=120)
# Why the button is restricted to have the same position in each group ?
- # position = models.IntegerField(default=0)
+ # position = models.IntegerField(default=0)
group = models.ManyToManyField(ButtonGroup)
class Meta:
return {
'label': self.label,
'tooltip': self.tooltip,
- 'accesskey': self.accesskey,
+ 'accesskey': self.accesskey,
'params': self.params,
- 'scriptlet_id': self.scriptlet_id
+ 'scriptlet_id': self.scriptlet_id,
}
def __unicode__(self):
return self.label
+
class Scriptlet(models.Model):
- name = models.CharField(max_length = 64, primary_key = True)
+ name = models.CharField(max_length=64, primary_key=True)
code = models.TextField()
def __unicode__(self):
# -*- coding: utf-8 -*-
#
# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
from django import template
from toolbar import models
register = template.Library()
+
@register.inclusion_tag('toolbar/toolbar.html')
def toolbar():
- return {'toolbar_groups': models.ButtonGroup.objects.all().select_related() }
+ return {'toolbar_groups': models.ButtonGroup.objects.all().select_related()}
+
@register.inclusion_tag('toolbar/button.html')
def toolbar_button(b):
# -*- coding: utf-8 -*-
#
# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
from django import forms
from wiki.models import Document, getstorage
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ugettext_lazy as _
class DocumentForm(forms.Form):
""" Old form for saving document's text """
-
+
name = forms.CharField(widget=forms.HiddenInput)
text = forms.CharField(widget=forms.Textarea)
revision = forms.IntegerField(widget=forms.HiddenInput)
comment = forms.CharField()
-
+
def __init__(self, *args, **kwargs):
document = kwargs.pop('instance', None)
super(DocumentForm, self).__init__(*args, **kwargs)
self.fields['name'].initial = document.name
self.fields['text'].initial = document.text
self.fields['revision'].initial = document.revision()
-
- def save(self, document_author = 'anonymous'):
+
+ def save(self, document_author='anonymous'):
storage = getstorage()
-
+
document = Document(storage, name=self.cleaned_data['name'], text=self.cleaned_data['text'])
-
- storage.put(document,
- author = document_author,
- comment = self.cleaned_data['comment'],
- parent =self.cleaned_data['revision'] )
-
+
+ storage.put(document,
+ author=document_author,
+ comment=self.cleaned_data['comment'],
+ parent=self.cleaned_data['revision'])
+
return storage.get(self.cleaned_data['name'])
-
+
+
class DocumentTagForm(forms.Form):
TAGS = (
- ("publish", "Do publikacji"),
+ ("publish", "Do publikacji"),
)
-
- tag = forms.ChoiceField(choices = TAGS)
- version = forms.IntegerField(widget = forms.HiddenInput)
+
+ tag = forms.ChoiceField(choices=TAGS)
+ version = forms.IntegerField(widget=forms.HiddenInput)
+
class DocumentTextSaveForm(forms.Form):
- """
+ """
Form for saving document's text:
-
+
* name - document's storage identifier.
* parent_revision - revision which the modified text originated from.
* comment - user's verbose comment; will be used in commit.
* stage_completed - mark this change as end of given stage.
-
+
"""
DOC_STAGES = (
('', 'Nic konkretnego'),
('tagging', 'Tagowanie'),
('modernized', 'Uwspółcześnienia'),
('editing', 'Redakcja'),
- )
-
+ )
+
id = forms.CharField(widget=forms.HiddenInput)
parent_revision = forms.IntegerField(widget=forms.HiddenInput)
text = forms.CharField(widget=forms.HiddenInput)
-
+
author = forms.CharField(
- required = False,
- label = _(u"Autor"),
- help_text = _(u"Twoje imie i nazwisko lub email.")
+ required=False,
+ label=_(u"Autor"),
+ help_text=_(u"Twoje imie i nazwisko lub email."),
)
-
+
comment = forms.CharField(
- required = True,
+ required=True,
widget=forms.Textarea,
- label = _(u"Twój komentarz"),
- help_text = _(u"Opisz w miarę dokładnie swoje zmiany."),
+ label=_(u"Twój komentarz"),
+ help_text=_(u"Opisz w miarę dokładnie swoje zmiany."),
)
-
+
stage_completed = forms.ChoiceField(
- choices=DOC_STAGES,
- required= False,
- label = _(u"Skończyłem robić"),
- help_text = _(u"Jeśli skończyłeś jeden z etapów utworu, wybierz go."),
+ choices=DOC_STAGES,
+ required=False,
+ label=_(u"Skończyłem robić"),
+ help_text=_(u"Jeśli skończyłeś jeden z etapów utworu, wybierz go."),
)
-
\ No newline at end of file
from django.template.loader import render_to_string
from datetime import datetime
+
class ExtendedEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Promise):
- return unicode(obj)
-
+ return unicode(obj)
+
if isinstance(obj, datetime):
return datetime.ctime(obj) + " " + (datetime.tzname(obj) or 'GMT')
-
+
return json.JSONEncoder.default(self, obj)
+
# shortcut for JSON reponses
class JSONResponse(http.HttpResponse):
-
- def __init__(self, data = {}, **kwargs):
+
+ def __init__(self, data={}, **kwargs):
# get rid of mimetype
kwargs.pop('mimetype', None)
-
+
super(JSONResponse, self).__init__(
- json.dumps(data, cls=ExtendedEncoder),
- mimetype = "application/json", **kwargs)
-
+ json.dumps(data, cls=ExtendedEncoder),
+ mimetype="application/json", **kwargs)
+
# return errors
class JSONFormInvalid(JSONResponse):
- def __init__(self, form):
- super(JSONFormInvalid, self).__init__(form.errors, status = 400)
-
-class JSONServerError(JSONResponse):
+ def __init__(self, form):
+ super(JSONFormInvalid, self).__init__(form.errors, status=400)
+
+
+class JSONServerError(JSONResponse):
def __init__(self, *args, **kwargs):
kwargs['status'] = 500
super(JSONServerError, self).__init__(*args, **kwargs)
-
-
-
\ No newline at end of file
# -*- coding: utf-8 -*-
#
# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
import re
import os
import logging
logger = logging.getLogger("fnp.wiki")
+
class DocumentStorage(object):
def __init__(self, path):
self.vstorage = vstorage.VersionedStorage(path)
- def get(self, name, revision = None):
+ def get(self, name, revision=None):
if revision is None:
text = self.vstorage.page_text(name)
else:
text = self.vstorage.revision_text(name, revision)
- return Document(self, name = name, text = text)
-
+ return Document(self, name=name, text=text)
+
def get_or_404(self, *args, **kwargs):
try:
return self.get(*args, **kwargs)
except DocumentNotFound:
- raise Http404
+ raise Http404
def put(self, document, author, comment, parent):
self.vstorage.save_text(
- title = document.name,
- text = document.text,
- author = author,
- comment = comment,
- parent = parent)
+ title=document.name,
+ text=document.text,
+ author=author,
+ comment=comment,
+ parent=parent)
def delete(self, name, author, comment):
self.vstorage.delete_page(name, author, comment)
def all(self):
return list(self.vstorage.all_pages())
-
+
def history(self, title):
return list(self.vstorage.page_history(title))
try:
return self.storage._info(self.name)[0]
except DocumentNotFound:
- return -1
-
+ return - 1
+
def add_tag(self, tag):
""" Add document specific tag """
logger.debug("Adding tag %s to doc %s version %d", tag, self.name, self.revision)
k, v = line.split(':', 1)
result[k.strip()] = v.strip()
except ValueError:
- continue
-
+ continue
+
gallery = result.get('gallery', self.name.replace(' ', '_'))
-
- if gallery.startswith('/'):
+
+ if gallery.startswith('/'):
gallery = os.path.basename(gallery)
-
+
result['gallery'] = gallery
-
+
if 'title' not in result:
- result['title'] = self.name.title()
+ result['title'] = self.name.title()
return result
-
+
def info(self):
return dict(zip(
('revision', 'last_update', 'last_comitter', 'commit_message'),
- self.storage._info(self.name)
- ))
+ self.storage._info(self.name),
+ ))
+
def getstorage():
return DocumentStorage(settings.REPOSITORY_PATH)
# -*- coding: utf-8 -*-
#
# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
import difflib
import re
from django.template.loader import render_to_string
from django.utils.html import escape as html_escape
-DIFF_RE = re.compile(r"""\x00([+^-])""" ,re.UNICODE)
-NAMES = { '+': 'added', '-': 'removed', '^': 'changed' }
+DIFF_RE = re.compile(r"""\x00([+^-])""", re.UNICODE)
+NAMES = {'+': 'added', '-': 'removed', '^': 'changed'}
+
def diff_replace(match):
- return """<span class="diff_mark diff_mark_%s">""" % NAMES[match.group(1)]
+ return """<span class="diff_mark diff_mark_%s">""" % NAMES[match.group(1)]
+
def filter_line(line):
- return DIFF_RE.sub(diff_replace, html_escape(line)).replace('\x01', '</span>')
+ return DIFF_RE.sub(diff_replace, html_escape(line)).replace('\x01', '</span>')
+
def html_diff_table(la, lb):
- return render_to_string("wiki/diff_table.html", {
- "changes": [(a[0],filter_line(a[1]),
- b[0],filter_line(b[1]),
- change) for a, b, change in difflib._mdiff(la, lb)]
- });
-
-
-__all__ = ['html_diff_table']
\ No newline at end of file
+ return render_to_string("wiki/diff_table.html", {
+ "changes": [(a[0], filter_line(a[1]), b[0], filter_line(b[1]), change)
+ for a, b, change in difflib._mdiff(la, lb)],
+ })
+
+
+__all__ = ['html_diff_table']
from nose.tools import *
import wiki.models as models
-import shutil, tempfile
+import shutil
+import tempfile
+
class TestStorageBase:
def setUp(self):
- self.dirpath = tempfile.mkdtemp(prefix = 'nosetest_')
+ self.dirpath = tempfile.mkdtemp(prefix='nosetest_')
def tearDown(self):
shutil.rmtree(self.dirpath)
+
class TestDocumentStorage(TestStorageBase):
def test_storage_empty(self):
storage = models.DocumentStorage(self.dirpath)
eq_(storage.all(), [])
-
-
-
-
from django.conf.urls.defaults import *
from django.conf import settings
-urlpatterns = patterns('wiki.views',
- url(r'^$',
+urlpatterns = patterns('wiki.views',
+ url(r'^$',
'document_list', name='wiki_doclist'),
- url(r'^gallery/(?P<directory>[^/]+)$',
+ url(r'^gallery/(?P<directory>[^/]+)$',
'document_gallery', name="wiki_gallery"),
- url(r'^(?P<name>[^/]+)/history$',
+ url(r'^(?P<name>[^/]+)/history$',
'document_history', name="wiki_history"),
- url(r'^(?P<name>[^/]+)/text$',
+ url(r'^(?P<name>[^/]+)/text$',
'document_text', name="wiki_text"),
- url(r'^(?P<name>[^/]+)/publish/(?P<version>\d+)$',
+ url(r'^(?P<name>[^/]+)/publish/(?P<version>\d+)$',
'document_publish', name="wiki_publish"),
- url(r'^(?P<name>[^/]+)/diff$',
- 'document_diff', name="wiki_diff"),
- url(r'^(?P<name>[^/]+)$',
- 'document_detail', name="wiki_details"),
-)
\ No newline at end of file
+ url(r'^(?P<name>[^/]+)/diff$',
+ 'document_diff', name="wiki_diff"),
+ url(r'^(?P<name>[^/]+)$',
+ 'document_detail', name="wiki_details"),
+)
MAX_LAST_DOCS = 10
+
@never_cache
-def document_list(request, template_name = 'wiki/document_list.html'):
+def document_list(request, template_name='wiki/document_list.html'):
# TODO: find a way to cache "Storage All"
- return direct_to_template(request, template_name, extra_context = {
+ return direct_to_template(request, template_name, extra_context={
'document_list': getstorage().all(),
- 'last_docs': sorted(request.session.get("wiki_last_docs", {}).items(),
- key=operator.itemgetter(1), reverse = True)
- })
+ 'last_docs': sorted(request.session.get("wiki_last_docs", {}).items(),
+ key=operator.itemgetter(1), reverse=True),
+ })
+
@never_cache
-def document_detail(request, name, template_name = 'wiki/document_details.html'):
-
+def document_detail(request, name, template_name='wiki/document_details.html'):
+
document = getstorage().get_or_404(name)
-
+
access_time = datetime.now()
- last_documents = request.session.get("wiki_last_docs", {})
+ last_documents = request.session.get("wiki_last_docs", {})
last_documents[name] = access_time
-
+
if len(last_documents) > MAX_LAST_DOCS:
- oldest_key = min(last_documents, key = last_documents.__getitem__)
- del last_documents[oldest_key]
- request.session['wiki_last_docs'] = last_documents
+ oldest_key = min(last_documents, key=last_documents.__getitem__)
+ del last_documents[oldest_key]
+ request.session['wiki_last_docs'] = last_documents
- return direct_to_template(request, template_name, extra_context = {
+ return direct_to_template(request, template_name, extra_context={
'document': document,
'document_info': document.info,
'document_meta': document.meta,
- 'forms': {"text_save": DocumentTextSaveForm(), "add_tag": DocumentTagForm() },
+ 'forms': {"text_save": DocumentTextSaveForm(), "add_tag": DocumentTagForm()},
})
+
@never_cache
-def document_text(request, name):
+def document_text(request, name):
storage = getstorage()
- document = storage.get_or_404(name)
-
- if request.method == 'POST':
+ document = storage.get_or_404(name)
+
+ if request.method == 'POST':
form = DocumentTextSaveForm(request.POST)
-
- if form.is_valid():
+
+ if form.is_valid():
revision = form.cleaned_data['parent_revision']
document.text = form.cleaned_data['text']
-
- storage.put(document,
- author = form.cleaned_data['author'] or request.user.username,
- comment = form.cleaned_data['comment'],
- parent = revision
+
+ storage.put(document,
+ author=form.cleaned_data['author'] or request.user.username,
+ comment=form.cleaned_data['comment'],
+ parent=revision,
)
-
+
return JSONResponse({
- 'text': document.plain_text if revision != document.revision() else None,
- 'meta': document.meta(),
- 'revision': document.revision()
+ 'text': document.plain_text if revision != document.revision() else None,
+ 'meta': document.meta(),
+ 'revision': document.revision(),
})
else:
- return JSONFormInvalid(form)
+ return JSONFormInvalid(form)
else:
return JSONResponse({
- 'text': document.plain_text,
- 'meta': document.meta(),
- 'revision': document.revision()
+ 'text': document.plain_text,
+ 'meta': document.meta(),
+ 'revision': document.revision(),
})
-
@never_cache
smart_unicode(settings.MEDIA_URL),
smart_unicode(settings.FILEBROWSER_DIRECTORY),
smart_unicode(directory)))
-
+
base_dir = os.path.join(
- smart_unicode(settings.MEDIA_ROOT),
+ smart_unicode(settings.MEDIA_ROOT),
smart_unicode(settings.FILEBROWSER_DIRECTORY),
smart_unicode(directory))
-
+
def map_to_url(filename):
return "%s/%s" % (base_url, smart_unicode(filename))
-
+
def is_image(filename):
return os.path.splitext(f)[1].lower() in (u'.jpg', u'.jpeg', u'.png')
-
- images = [ map_to_url(f) for f in map(smart_unicode, os.listdir(base_dir)) if is_image(f) ]
+
+ images = [map_to_url(f) for f in map(smart_unicode, os.listdir(base_dir)) if is_image(f)]
images.sort()
return JSONResponse(images)
except (IndexError, OSError), exc:
import traceback
traceback.print_exc()
raise http.Http404
-
+
+
@never_cache
def document_diff(request, name):
- storage = getstorage()
-
+ storage = getstorage()
+
revA = int(request.GET.get('from', 0))
revB = int(request.GET.get('to', 0))
-
+
if revA > revB:
revA, revB = revB, revA
-
+
if revB == 0:
- revB = None
-
+ revB = None
+
docA = storage.get_or_404(name, int(revA))
- docB = storage.get_or_404(name, int(revB))
-
- return http.HttpResponse(nice_diff.html_diff_table(docA.plain_text.splitlines(),
- docB.plain_text.splitlines()) )
-
-@never_cache
+ docB = storage.get_or_404(name, int(revB))
+
+ return http.HttpResponse(nice_diff.html_diff_table(docA.plain_text.splitlines(),
+ docB.plain_text.splitlines()))
+
+
+@never_cache
def document_history(request, name):
storage = getstorage()
- return JSONResponse(storage.history(name))
+ return JSONResponse(storage.history(name))
+
@require_POST
-def document_add_tag(request, name):
+def document_add_tag(request, name):
storage = getstorage()
-
+
form = DocumentTagForm(request.POST)
if form.is_valid():
doc = storage.get_or_404(name, form.cleaned_data['version'])
doc.add_tag(form.cleaned_data['tag'])
- return JSONResponse({"message": _("Tag added")})
+ return JSONResponse({"message": _("Tag added")})
else:
return JSONFormInvalid(form)
-
+
+
@require_POST
def document_publish(request, name, version):
storage = getstorage()
-
+
# get the document
- document = storage.get_or_404(name, revision = int(version))
-
- api = wlapi.WLAPI(settings.WL_API_CONFIG)
- try:
+ document = storage.get_or_404(name, revision=int(version))
+
+ api = wlapi.WLAPI(settings.WL_API_CONFIG)
+ try:
return JSONResponse({"result": api.publish_book(document)})
- except wlapi.APICallException, e:
- return JSONServerError({"message": str(e)})
\ No newline at end of file
+ except wlapi.APICallException, e:
+ return JSONServerError({"message": str(e)})
-from __future__ import with_statement # needed for python 2.5
+from __future__ import with_statement # needed for python 2.5
from fabric.api import *
from fabric.contrib import files
env.project_name = 'platforma'
env.use_south = True
+
# Servers
def staging():
"""Use staging server"""
env.virtualenv = '/usr/bin/virtualenv'
env.pip = '/usr/bin/pip'
+
def production():
"""Use production server"""
env.hosts = ['szo.nowoczesnapolska.org.pl:2225']
# =========
def test():
"Run the test suite and bail out if it fails"
- require('hosts', 'path', provided_by = [staging, production])
+ require('hosts', 'path', provided_by=[staging, production])
result = run('cd %(path)s/%(project_name)s; %(python)s manage.py test' % env)
+
def setup():
"""
Setup a fresh virtualenv as well as a few useful directories, then run
a full deployment. virtualenv and pip should be already installed.
"""
- require('hosts', 'path', provided_by = [staging, production])
+ require('hosts', 'path', provided_by=[staging, production])
- run('mkdir -p %(path)s; cd %(path)s; %(virtualenv)s --no-site-packages .;' % env, pty = True)
- run('cd %(path)s; mkdir releases; mkdir shared; mkdir packages;' % env, pty = True)
- run('cd %(path)s/releases; ln -s . current; ln -s . previous' % env, pty = True)
+ run('mkdir -p %(path)s; cd %(path)s; %(virtualenv)s --no-site-packages .;' % env, pty=True)
+ run('cd %(path)s; mkdir releases; mkdir shared; mkdir packages;' % env, pty=True)
+ run('cd %(path)s/releases; ln -s . current; ln -s . previous' % env, pty=True)
deploy()
+
def deploy():
"""
- Deploy the latest version of the site to the servers,
- install any required third party modules,
+ Deploy the latest version of the site to the servers,
+ install any required third party modules,
install the virtual host and then restart the webserver
"""
- require('hosts', 'path', provided_by = [staging, production])
+ require('hosts', 'path', provided_by=[staging, production])
import time
env.release = time.strftime('%Y-%m-%dT%H%M')
django_compress()
restart_webserver()
+
def deploy_version(version):
"Specify a specific version to be made live"
- require('hosts', 'path', provided_by = [localhost, webserver])
+ require('hosts', 'path', provided_by=[localhost, webserver])
env.version = version
with cd(env.path):
- run('rm releases/previous; mv releases/current releases/previous;', pty = True)
- run('ln -s %(version)s releases/current' % env, pty = True)
+ run('rm releases/previous; mv releases/current releases/previous;', pty=True)
+ run('ln -s %(version)s releases/current' % env, pty=True)
restart_webserver()
+
def rollback():
"""
Limited rollback capability. Simple loads the previously current
version of the code. Rolling back again will swap between the two.
"""
- require('hosts', provided_by = [staging, production])
+ require('hosts', provided_by=[staging, production])
require('path')
with cd(env.path):
- run('mv releases/current releases/_previous;', pty = True)
- run('mv releases/previous releases/current;', pty = True)
- run('mv releases/_previous releases/previous;', pty = True)
+ run('mv releases/current releases/_previous;', pty=True)
+ run('mv releases/previous releases/current;', pty=True)
+ run('mv releases/_previous releases/previous;', pty=True)
restart_webserver()
def upload_tar_from_git():
"Create an archive from the current Git master branch and upload it"
print '>>> upload tar from git'
- require('release', provided_by = [deploy])
+ require('release', provided_by=[deploy])
local('git archive --format=tar master | gzip > %(release)s.tar.gz' % env)
- run('mkdir -p %(path)s/releases/%(release)s' % env, pty = True)
- run('mkdir -p %(path)s/packages' % env, pty = True)
+ run('mkdir -p %(path)s/releases/%(release)s' % env, pty=True)
+ run('mkdir -p %(path)s/packages' % env, pty=True)
put('%(release)s.tar.gz' % env, '%(path)s/packages/' % env)
- run('cd %(path)s/releases/%(release)s && tar zxf ../../packages/%(release)s.tar.gz' % env, pty = True)
+ run('cd %(path)s/releases/%(release)s && tar zxf ../../packages/%(release)s.tar.gz' % env, pty=True)
local('rm %(release)s.tar.gz' % env)
+
def upload_vhost_sample():
"Create and upload Apache virtual host configuration sample"
print ">>> upload vhost sample"
- files.upload_template('%(project_name)s.vhost.template' % env, '%(path)s/%(project_name)s.vhost.sample' % env, context = env)
+ files.upload_template('%(project_name)s.vhost.template' % env, '%(path)s/%(project_name)s.vhost.sample' % env, context=env)
+
def upload_wsgi_script():
"Create and upload a wsgi script sample"
print ">>> upload wsgi script sample"
- files.upload_template('%(project_name)s.wsgi.template' % env, '%(path)s/%(project_name)s.wsgi' % env, context = env)
+ files.upload_template('%(project_name)s.wsgi.template' % env, '%(path)s/%(project_name)s.wsgi' % env, context=env)
run('chmod ug+x %(path)s/%(project_name)s.wsgi' % env)
+
def install_requirements():
"Install the required packages from the requirements file using pip"
print '>>> install requirements'
- require('release', provided_by = [deploy])
- run('cd %(path)s; %(pip)s install -E . -r %(path)s/releases/%(release)s/requirements.txt' % env, pty = True)
+ require('release', provided_by=[deploy])
+ run('cd %(path)s; %(pip)s install -E . -r %(path)s/releases/%(release)s/requirements.txt' % env, pty=True)
+
def copy_localsettings():
"Copy localsettings.py from root directory to release directory (if this file exists)"
print ">>> copy localsettings"
- require('release', provided_by = [deploy])
- require('path', provided_by = [staging, production])
+ require('release', provided_by=[deploy])
+ require('path', provided_by=[staging, production])
- with settings(warn_only = True):
+ with settings(warn_only=True):
run('cp %(path)s/localsettings.py %(path)s/releases/%(release)s/%(project_name)s' % env)
+
def symlink_current_release():
"Symlink our current release"
print '>>> symlink current release'
- require('release', provided_by = [deploy])
- require('path', provided_by = [staging, production])
+ require('release', provided_by=[deploy])
+ require('path', provided_by=[staging, production])
with cd(env.path):
run('rm releases/previous; mv releases/current releases/previous')
run('ln -s %(release)s releases/current' % env)
+
def migrate():
"Update the database"
print '>>> migrate'
- require('project_name', provided_by = [staging, production])
+ require('project_name', provided_by=[staging, production])
with cd('%(path)s/releases/current/%(project_name)s' % env):
- run('../../../bin/python manage.py syncdb --noinput' % env, pty = True)
+ run('../../../bin/python manage.py syncdb --noinput' % env, pty=True)
if env.use_south:
- run('../../../bin/python manage.py migrate' % env, pty = True)
-
+ run('../../../bin/python manage.py migrate' % env, pty=True)
+
+
def django_compress():
"Update static files"
print '>>> migrate'
- require('project_name', provided_by = [staging, production])
+ require('project_name', provided_by=[staging, production])
with cd('%(path)s/releases/current/%(project_name)s' % env):
- run('../../../bin/python manage.py synccompress --force' % env, pty = True)
-
+ run('../../../bin/python manage.py synccompress --force' % env, pty=True)
+
def restart_webserver():
"Restart the web server"
# -*- coding: utf-8 -*-
#
# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
import os
import vstorage
+NULL_PARENT = -1
+
def clear_directory(top):
for root, dirs, files in os.walk(top, topdown=False):
def setUp(self):
self.repo_path = tempfile.mkdtemp()
self.repo = vstorage.VersionedStorage(self.repo_path)
-
+
def tearDown(self):
clear_directory(self.repo_path)
-
+
def test_save_text(self):
text = u"test text"
title = u"test title"
author = u"test author"
comment = u"test comment"
-
- self.repo.save_text(title = title,
- text = text, author = author,
- comment = comment, parent=-1)
-
+
+ self.repo.save_text(
+ title=title,
+ text=text,
+ author=author,
+ comment=comment,
+ parent=NULL_PARENT,
+ )
+
saved = self.repo.open_page(title).read()
assert_equal(saved, text)
title = u"test title"
author = u"test author"
comment = u"test comment"
-
- self.repo.save_text(title = title,
- text = text, author = author,
- comment = comment, parent=None)
-
+
+ self.repo.save_text(title=title,
+ text=text, author=author,
+ comment=comment, parent=None)
+
saved = self.repo.open_page(title).read()
assert_equal(saved, text)
title = u"test title"
author = u"test author"
comment = u"test comment"
- self.repo.save_text(title = title,
- text = text, author = author,
- comment = comment, parent=-1)
- self.repo.save_text(title = title,
- text = text, author = author,
- comment = comment, parent=-1)
+ self.repo.save_text(title=title,
+ text=text, author=author,
+ comment=comment, parent=NULL_PARENT)
+ self.repo.save_text(title=title,
+ text=text, author=author,
+ comment=comment, parent=NULL_PARENT)
saved = self.repo.open_page(title).read()
assert_equal(saved, text)
-
+
def test_save_merge_line_conflict(self):
text = u"test\ntest\n"
text1 = u"test\ntext\n"
title = u"test title"
author = u"test author"
comment = u"test comment"
-
- self.repo.save_text(title = title,
- text = text, author = author,
- comment = comment, parent=-1)
-
- self.repo.save_text(title = title,
- text = text1, author = author,
- comment = comment, parent=0)
-
- self.repo.save_text(title = title,
- text = text2, author = author,
- comment = comment, parent=0)
-
+
+ self.repo.save_text(title=title,
+ text=text, author=author,
+ comment=comment, parent=NULL_PARENT)
+
+ self.repo.save_text(title=title,
+ text=text1, author=author,
+ comment=comment, parent=0)
+
+ self.repo.save_text(title=title,
+ text=text2, author=author,
+ comment=comment, parent=0)
+
saved = self.repo.open_page(title).read()
-
+
# Other conflict markers placement can also be correct
assert_equal(saved, u'''\
text
>>>>>>> other
''')
-
def test_delete(self):
text = u"text test"
title = u"test title"
author = u"test author"
comment = u"test comment"
- self.repo.save_text(title = title,
- text = text, author = author,
- comment = comment, parent=-1)
-
+ self.repo.save_text(title=title,
+ text=text, author=author,
+ comment=comment, parent=NULL_PARENT)
+
assert title in self.repo
-
+
self.repo.delete_page(title, author, comment)
-
+
assert title not in self.repo
@raises(vstorage.DocumentNotFound)
self.repo.open_page(u'unknown entity')
def test_open_existing_repository(self):
- self.repo.save_text(title = u'Python!', text = u'ham and spam')
+ self.repo.save_text(title=u'Python!', text=u'ham and spam')
current_repo_revision = self.repo.repo_revision()
same_repo = vstorage.VersionedStorage(self.repo_path)
assert_equal(same_repo.repo_revision(), current_repo_revision)
-
-
+
def test_history(self):
COMMITS = [
- {"author": "bunny", "text":"1", "comment": "Oh yeah!"},
+ {"author": "bunny", "text":"1", "comment": "Oh yeah!"},
{"author": "frank", "text":"2", "comment": "Second is the best!"},
- {"text":"3", "comment": "Third"}, # anonymous
- {"author": "welma", "text":"4", "comment": "Fourth"},
+ {"text":"3", "comment": "Third"},
+ {"author": "welma", "text":"4", "comment": "Fourth"},
]
-
+
for commit in COMMITS:
- self.repo.save_text(title = u"Sample", **commit)
-
+ self.repo.save_text(title=u"Sample", **commit)
+
for n, entry in enumerate(reversed(list(self.repo.page_history(u"Sample")))):
assert_equal(entry["version"], n)
- assert_equal(entry["author"], COMMITS[n].get("author", "anonymous") )
+ assert_equal(entry["author"], COMMITS[n].get("author", "anonymous"))
assert_equal(entry["description"], COMMITS[n]["comment"])
- assert_equal(entry["tag"], [])
-
-
+ assert_equal(entry["tag"], [])
+
+
class TestVSTags(object):
-
+
TITLE_1 = "Sample"
-
+
COMMITS = [
- {"author": "bunny", "text":"1", "comment": "Oh yeah!"},
+ {"author": "bunny", "text":"1", "comment": "Oh yeah!"},
{"author": "frank", "text":"2", "comment": "Second is the best!"},
- {"text":"3", "comment": "Third"}, # anonymous
- {"author": "welma", "text":"4", "comment": "Fourth"},
- ]
-
+ {"text":"3", "comment": "Third"},
+ {"author": "welma", "text":"4", "comment": "Fourth"},
+ ]
+
def setUp(self):
self.repo_path = tempfile.mkdtemp()
self.repo = vstorage.VersionedStorage(self.repo_path)
-
+
# generate some history
for commit in self.COMMITS:
- self.repo.save_text(title = u"Sample", **commit)
-
+ self.repo.save_text(title=u"Sample", **commit)
+
# verify
for n, entry in enumerate(reversed(list(self.repo.page_history(self.TITLE_1)))):
assert_equal(entry["tag"], [])
-
+
def tearDown(self):
clear_directory(self.repo_path)
-
- def test_add_tag(self):
+
+ def test_add_tag(self):
TAG_USER = "mike_the_tagger"
TAG_NAME = "production"
TAG_VERSION = 2
-
+
# Add tag
self.repo.add_page_tag(self.TITLE_1, TAG_VERSION, TAG_NAME, TAG_USER)
-
+
# check history again
history = list(self.repo.page_history(self.TITLE_1))
for entry in reversed(history):
if entry["version"] == TAG_VERSION:
assert_equal(entry["tag"], [TAG_NAME])
else:
- assert_equal(entry["tag"], [])
-
- def test_add_many_tags(self):
+ assert_equal(entry["tag"], [])
+
+ def test_add_many_tags(self):
TAG_USER = "mike_the_tagger"
tags = [
(2, "production", "mike"),
(2, "finished", "jeremy"),
(0, "original", "jeremy"),
]
-
-
+
for rev, name, user in tags:
self.repo.add_page_tag(self.TITLE_1, rev, name, user)
-
+
# check history again
history = list(self.repo.page_history(self.TITLE_1))
for entry in reversed(history):
- expected = [ tag[1] for tag in tags if tag[0] == entry["version"] ]
+ expected = [tag[1] for tag in tags if tag[0] == entry["version"]]
assert_equal(set(entry["tag"]), set(expected))
if __name__ == '__main__':
- runmodule()
\ No newline at end of file
+ runmodule()
# -*- coding: utf-8 -*-
#
# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
import os
import tempfile
import mercurial.util
-def urlquote(url, safe = '/'):
- """Quotes URL
-
+def urlquote(url, safe='/'):
+ """Quotes URL
+
>>> urlquote(u'Za\u017c\xf3\u0142\u0107 g\u0119\u015bl\u0105 ja\u017a\u0144')
'Za%C5%BC%C3%B3%C5%82%C4%87_g%C4%99%C5%9Bl%C4%85_ja%C5%BA%C5%84'
"""
def urlunquote(url):
- """Unqotes URL
-
+ """Unqotes URL
+
# >>> urlunquote('Za%C5%BC%C3%B3%C5%82%C4%87_g%C4%99%C5%9Bl%C4%85_ja%C5%BA%C5%84')
# u'Za\u017c\xf3\u0142\u0107 g\u0119\u015bl\u0105 ja\u017a\u0144'
"""
wlock = self.repo.wlock()
try:
return func(self, *args, **kwargs)
- finally:
+ finally:
wlock.release()
return wrapped
+
def with_storage_locked(func):
"""A decorator for locking the repository when calling a method."""
lock = self.repo.lock()
try:
return func(self, *args, **kwargs)
- finally:
+ finally:
lock.release()
return wrapped
+
def guess_mime(file_name):
"""
Guess file's mime type based on extension.
'archive/gzip'
"""
- mime, encoding = mimetypes.guess_type(file_name, strict = False)
+ mime, encoding = mimetypes.guess_type(file_name, strict=False)
if encoding:
mime = 'archive/%s' % encoding
if mime is None:
change history, using Mercurial repository as the storage method.
"""
- def __init__(self, path, charset = None):
+ def __init__(self, path, charset=None):
"""
Takes the path to the directory where the pages are to be kept.
If the directory doen't exist, it will be created. If it's inside
if not os.path.exists(self.path):
os.makedirs(self.path)
self.repo_path = find_repo_path(self.path)
-
+
self.ui = mercurial.ui.ui()
self.ui.quiet = True
self.ui._report_untrusted = False
self.ui.setconfig('ui', 'interactive', False)
-
+
if self.repo_path is None:
self.repo_path = self.path
create = True
else:
create = False
-
+
self.repo_prefix = self.path[len(self.repo_path):].strip('/')
self.repo = mercurial.hg.repository(self.ui, self.repo_path,
- create = create)
+ create=create)
def reopen(self):
"""Close and reopen the repo, to make sure we are up to date."""
self.repo = mercurial.hg.repository(self.ui, self.repo_path)
def _file_path(self, title):
- return os.path.join(self.path, urlquote(title, safe = ''))
+ return os.path.join(self.path, urlquote(title, safe=''))
def _title_to_file(self, title):
- return os.path.join(self.repo_prefix, urlquote(title, safe = ''))
+ return os.path.join(self.repo_prefix, urlquote(title, safe=''))
def _file_to_title(self, filename):
assert filename.startswith(self.repo_prefix)
name = filename[len(self.repo_prefix):].strip('/')
return urlunquote(name)
- def __contains__(self, title):
+ def __contains__(self, title):
return urlquote(title) in self.repo['tip']
def __iter__(self):
@with_working_copy_locked
@with_storage_locked
- def save_file(self, title, file_name, author = u'', comment = u'', parent = None):
+ def save_file(self, title, file_name, author=u'', comment=u'', parent=None):
"""Save an existing file as specified page."""
user = author.encode('utf-8') or u'anonymous'.encode('utf-8')
text = comment.encode('utf-8') or u'comment'.encode('utf-8')
-
+
repo_file = self._title_to_file(title)
file_path = self._file_path(title)
mercurial.util.rename(file_name, file_path)
changectx = self._changectx()
-
+
try:
filectx_tip = changectx[repo_file]
current_page_rev = filectx_tip.filerev()
except mercurial.revlog.LookupError:
self.repo.add([repo_file])
current_page_rev = -1
-
+
if parent is not None and current_page_rev != parent:
msg = self.merge_changes(changectx, repo_file, text, user, parent)
user = '<wiki>'
text = msg.encode('utf-8')
-
+
self._commit([repo_file], text, user)
-
-
+
def save_data(self, title, data, **kwargs):
"""Save data as specified page."""
try:
- temp_path = tempfile.mkdtemp(dir = self.path)
+ temp_path = tempfile.mkdtemp(dir=self.path)
file_path = os.path.join(temp_path, 'saved')
f = open(file_path, "wb")
f.write(data)
f.close()
- self.save_file(title = title, file_name = file_path, **kwargs)
+ self.save_file(title=title, file_name=file_path, **kwargs)
finally:
try:
os.unlink(file_path)
pass
def save_text(self, text, **kwargs):
- """Save text as specified page, encoded to charset."""
- self.save_data(data = text.encode(self.charset), **kwargs)
-
+ """Save text as specified page, encoded to charset."""
+ self.save_data(data=text.encode(self.charset), **kwargs)
def _commit(self, files, text, user):
match = mercurial.match.exact(self.repo_path, '', list(files))
- return self.repo.commit(match = match, text = text, user = user, force = True)
-
+ return self.repo.commit(match=match, text=text, user=user, force=True)
+
def page_text(self, title):
"""Read unicode text of a page."""
data = self.open_page(title).read()
@with_working_copy_locked
@with_storage_locked
- def delete_page(self, title, author = u'', comment = u''):
+ def delete_page(self, title, author=u'', comment=u''):
user = author.encode('utf-8') or 'anon'
text = comment.encode('utf-8') or 'deleted'
repo_file = self._title_to_file(title)
def open_page(self, title):
if title not in self:
raise DocumentNotFound()
-
+
path = self._title_to_file(title)
- logger.debug("Opening page %s", path)
+ logger.debug("Opening page %s", path)
try:
- return self.repo.wfile(path, 'rb')
+ return self.repo.wfile(path, 'rb')
except IOError:
logger.exception("Failed to open page %s", title)
raise DocumentNotFound()
except OSError:
return 0, 0, 0
return st_ino, st_size, st_mtime
-
+
@with_working_copy_locked
def page_meta(self, title):
"""Get page's revision, date, last editor and his edit comment."""
def repo_revision(self):
return self.repo['tip'].rev()
-
+
def _changectx(self):
return self.repo['tip']
"""
return guess_mime(self._file_path(title))
- def _find_filectx(self, title, rev = None):
+ def _find_filectx(self, title, rev=None):
"""Find the last revision in which the file existed."""
repo_file = self._title_to_file(title)
for parent in changectx.parents():
if parent != changectx:
stack.append(parent)
-
+
try:
- fctx = changectx[repo_file]
+ fctx = changectx[repo_file]
return fctx if rev is None else fctx.filectx(rev)
except IndexError, LookupError:
raise DocumentNotFound()
"""Iterate over the page's history."""
filectx_tip = self._find_filectx(title)
-
+
maxrev = filectx_tip.filerev()
minrev = 0
for rev in range(maxrev, minrev - 1, -1):
'replace').split('<')[0].strip()
comment = unicode(filectx.description(), "utf-8", 'replace')
tags = [t.rsplit('#', 1)[-1] for t in filectx.changectx().tags() if '#' in t]
-
+
yield {
- "version": rev,
- "date": date,
- "author": author,
+ "version": rev,
+ "date": date,
+ "author": author,
"description": comment,
"tag": tags,
}
data = self.page_revision(title, rev)
text = unicode(data, self.charset, 'replace')
return text
-
+
@with_working_copy_locked
- def add_page_tag(self, title, rev, tag, user = "<wiki>", doctag = True):
+ def add_page_tag(self, title, rev, tag, user="<wiki>", doctag=True):
if doctag:
tag = "{title}#{tag}".format(**locals())
-
+
message = "Assigned tag {tag} to version {rev} of {title}".format(**locals())
-
+
fctx = self._find_filectx(title, rev)
self.repo.tag(
- names = tag, node = fctx.node(), local = False,
- user = user, message = message, date = None
- )
+ names=tag, node=fctx.node(), local=False,
+ user=user, message=message, date=None,
+ )
def history(self):
"""Iterate over the history of entire wiki."""
def all_pages(self):
tip = self.repo['tip']
"""Iterate over the titles of all pages in the wiki."""
- return [ urlunquote(filename) for filename in tip ]
+ return [urlunquote(filename) for filename in tip]
def changed_since(self, rev):
"""Return all pages that changed since specified repository revision."""
# -*- coding: utf-8 -*-
#
# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
"""
Abstraction over API for wolnelektury.pl
import logging
logger = logging.getLogger("fnp.lib.wlapi")
+
class APICallException(Exception):
- pass
+ pass
+
def api_call(path, format="json"):
def wrapper(func):
-
+
@functools.wraps(func)
- def wrapped(self, *args, **kwargs):
+ def wrapped(self, *args, **kwargs):
generator = func(self, *args, **kwargs)
-
+
data = generator.next()
-
+
# prepare request
rq = urllib2.Request(self.base_url + path + ".json")
-
+
# will send POST when there is data, GET otherwise
if data is not None:
- rq.add_data(json.dumps(data))
+ rq.add_data(json.dumps(data))
rq.add_header("Content-Type", "application/json")
-
+
try:
anwser = json.load(self.opener.open(rq))
return generator.send(anwser)
except StopIteration:
# by default, just return the anwser as a shorthand
- return anwser
+ return anwser
except urllib2.HTTPError, error:
- return self._http_error(error)
+ return self._http_error(error)
except Exception, error:
- return self._error(error)
+ return self._error(error)
return wrapped
-
+
return wrapper
-
-
+
class WLAPI(object):
-
- def __init__(self, config_dict):
+
+ def __init__(self, config_dict):
self.base_url = config_dict['URL']
self.auth_realm = config_dict['AUTH_REALM']
- self.auth_user = config_dict['AUTH_USER']
-
- auth_handler = urllib2.HTTPDigestAuthHandler();
+ self.auth_user = config_dict['AUTH_USER']
+
+ auth_handler = urllib2.HTTPDigestAuthHandler()
auth_handler.add_password(
realm=self.auth_realm, uri=self.base_url,
user=self.auth_user, passwd=config_dict['AUTH_PASSWD'])
-
+
self.opener = urllib2.build_opener(auth_handler)
-
+
def _http_error(self, error):
return self._error()
-
+
def _error(self, error):
- raise APICallException(cause = error)
-
+ raise APICallException(cause=error)
+
@api_call("books")
def publish_book(self, document):
- yield {"text": document.text, "compressed": False}
-
-
\ No newline at end of file
+ yield {"text": document.text, "compressed": False}
# -*- coding: utf-8
-__author__="lreqc"
-__date__ ="$2009-09-03 08:34:10$"
+
def settings(request):
from django.conf import settings
- return {'MEDIA_URL': settings.MEDIA_URL,
+ return {'MEDIA_URL': settings.MEDIA_URL,
'STATIC_URL': settings.STATIC_URL,
- 'REDMINE_URL': settings.REDMINE_URL }
-
-
+ 'REDMINE_URL': settings.REDMINE_URL}
#!/usr/bin/env python
from django.core.management import execute_manager
try:
- import settings # Assumed to be in the same directory.
+ import settings # Assumed to be in the same directory.
except ImportError:
import sys
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
# Append lib and apps directories to PYTHONPATH
from os import path
import sys
-
+
PROJECT_ROOT = path.realpath(path.dirname(__file__))
sys.path += [PROJECT_ROOT + '/../apps', PROJECT_ROOT + '/../lib']
-
+
execute_manager(settings)
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django_cas.middleware.CASMiddleware',
- 'django.middleware.doc.XViewMiddleware',
+ 'django.middleware.doc.XViewMiddleware',
'maintenancemode.middleware.MaintenanceModeMiddleware',
# 'debug_toolbar.middleware.DebugToolbarMiddleware' #
)
#
# Central Auth System
#
-## Set this to where the CAS server lives
+## Set this to where the CAS server lives
# CAS_SERVER_URL = "http://cas.fnp.pl/
CAS_ADMIN_PREFIX = "/admin/"
CAS_LOGOUT_COMPLETELY = True
'detail': {
'source_filenames': (
'css/master.css',
- 'css/gallery.css',
- 'css/history.css',
+ 'css/gallery.css',
+ 'css/history.css',
'css/summary.css',
- 'css/html.css',
+ 'css/html.css',
'css/jquery.autocomplete.css',
'css/dialogs.css',
),
},
'listing': {
'source_filenames': (
- 'css/filelist.css',
- ),
+ 'css/filelist.css',
+ ),
'output_filename': 'compressed/listing_styles_?.css',
}
}
-
+
COMPRESS_JS = {
# everything except codemirror
'detail': {
'source_filenames': (
- 'js/jquery-1.4.2.min.js',
- 'js/jquery.autocomplete.js',
+ 'js/jquery-1.4.2.min.js',
+ 'js/jquery.autocomplete.js',
'js/jquery.blockui.js',
- 'js/jquery.elastic.js',
+ 'js/jquery.elastic.js',
'js/button_scripts.js',
'js/slugify.js',
-
+
# wiki scripts
'js/wiki/wikiapi.js',
'js/wiki/base.js',
'js/wiki/xslt.js',
-
+
# dialogs
'js/wiki/save_dialog.js',
-
+
# views
'js/wiki/history_view.js',
'js/wiki/summary_view.js',
'js/wiki/source_editor.js',
'js/wiki/wysiwyg_editor.js',
- 'js/wiki/scan_gallery.js',
+ 'js/wiki/scan_gallery.js',
'js/wiki/diff_view.js',
-
+
# bootstrap
'js/wiki/main.js',
- ),
+ ),
'output_filename': 'compressed/detail_scripts_?.js',
},
'listing': {
'source_filenames': (
- 'js/jquery-1.4.2.min.js',
- 'js/slugify.js',
- ),
+ 'js/jquery-1.4.2.min.js',
+ 'js/slugify.js',
+ ),
'output_filename': 'compressed/listing_scripts_?.js',
}
}
-
+
COMPRESS = True
COMPRESS_CSS_FILTERS = None
COMPRESS_JS_FILTERS = None
'django_nose',
'debug_toolbar',
-
+
'compress',
'south',
'sorl.thumbnail',
'filebrowser',
- 'wiki',
+ 'wiki',
'toolbar',
)
'--cover-package=' + ','.join(TEST_MODULES),
'-d',
'--with-coverage',
- '--with-doctest'
+ '--with-doctest',
)
pass
try:
- LOGGING_CONFIG_FILE
+ LOGGING_CONFIG_FILE
except NameError:
- LOGGING_CONFIG_FILE = os.path.join(PROJECT_ROOT, 'config',
+ LOGGING_CONFIG_FILE = os.path.join(PROJECT_ROOT, 'config',
('logging.cfg' if not DEBUG else 'logging.cfg.dev'))
try:
import logging
import logging.config
-
- logging.config.fileConfig(LOGGING_CONFIG_FILE)
+
+ logging.config.fileConfig(LOGGING_CONFIG_FILE)
except ImportError, exc:
- raise
\ No newline at end of file
+ raise
urlpatterns = patterns('',
# Auth
- url(r'^accounts/login/$', 'django_cas.views.login', name = 'login'),
- url(r'^accounts/logout/$', 'django_cas.views.logout', name = 'logout'),
+ url(r'^accounts/login/$', 'django_cas.views.login', name='login'),
+ url(r'^accounts/logout/$', 'django_cas.views.logout', name='logout'),
# Admin panel
(r'^admin/filebrowser/', include('filebrowser.urls')),
# -*- coding: utf-8 -*-
#
# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
import sys
import os
bottom = int(height * bottom)
if left < 1:
left = int(width * left)
-
+
bounds = (int(left), int(top), int(width - right), int(height - bottom))
image = image.crop(bounds)
image.load()
return image
-
output_dir = realpath(os.getcwd()) + '/output'
bounds = [float(i) for i in sys.argv[1].split(':')]
except IOError, e:
sys.stderr.write('\nerror:%s:%s\n' % (file_name, e.message))
continue
-
+
image = crop(image, *bounds)
- image.save(output_dir + '/' + basename(file_name))
\ No newline at end of file
+ image.save(output_dir + '/' + basename(file_name))
# -*- coding: utf-8 -*-
#
# This file is part of FNP-Redakcja, licensed under GNU Affero GPLv3 or later.
-# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
import sys
import os
"""Return width to height ratio of image."""
width, height = image.size
return float(width) / height
-
+
def try_creating(directory):
try:
except IOError, e:
sys.stderr.write('\nerror:%s:%s\n' % (file_name, e.message))
continue
-
+
# Check ratio
if ratio(image) > 1:
images = [crop(image, 0.6), crop(image, 0.6, from_right=True)]
else:
images = [image]
-
+
for i, image in enumerate(images):
image_name = '%s.%d.png' % (basename(base_name), i)
-
+
# Save files
small_image = resize(image, 640, 960)
small_image = small_image.convert('L')
tmp_output_dir + '/' + image_name,
))
os.remove(tmp_output_dir + '/' + image_name)
-
+
# big_image = resize(image, 960, 1440)
# big_image = big_image.convert('L')
# big_image = big_image.filter(ImageFilter.SHARPEN)
# tmp_output_dir + '/' + image_name,
# ))
# os.remove(tmp_output_dir + '/' + image_name)
-
+
sys.stderr.write('.')
shutil.rmtree(tmp_output_dir)
import os
from distutils.core import setup
+RESOURCE_PATHS = ('templates', 'static', 'media', 'locale', 'config')
+
+
def is_package(path):
- return (
- os.path.isdir(path) and
- os.path.isfile(os.path.join(path, '__init__.py'))
- )
+ return os.path.isdir(path) \
+ and os.path.isfile(os.path.join(path, '__init__.py'))
-def find_packages(path, base="" ):
+
+def find_packages(path, base=""):
""" Find all packages in path """
packages = {}
for item in os.listdir(path):
dir = os.path.join(path, item)
- if is_package( dir ):
+ if is_package(dir):
if base:
module_name = "%(base)s.%(item)s" % vars()
else:
packages.update(find_packages(dir, module_name))
return packages
+
def files_from(*paths, **kwargs):
- base = kwargs.pop('base')
+ base = kwargs.pop('base')
+
def generator():
for path in paths:
- path = os.path.join(base, path)
- if not os.path.isdir(path) or is_package(path): continue
-
- for dir, _, files in os.walk(path):
- for file in files:
- yield os.path.relpath(os.path.join(dir, file),base)
-
+ path = os.path.join(base, path)
+ if not os.path.isdir(path) or is_package(path):
+ continue
+
+ for dir, _, files in os.walk(path):
+ for file in files:
+ yield os.path.relpath(os.path.join(dir, file), base)
+
return list(generator())
-RESOURCE_PATHS = ('templates', 'static', 'media', 'locale', 'config')
-def django_setup(project, apps=[], apps_dir='apps', package_dir = {}, packages = [], package_data = {}, **kwargs):
-
+def django_setup(project, apps=[], apps_dir='apps', package_dir={}, packages=[], package_data={}, **kwargs):
+
# directories
- extra_dirs = dict( (app, os.path.join(apps_dir,app)) for app in apps )
+ extra_dirs = dict((app, os.path.join(apps_dir, app)) for app in apps)
extra_dirs[project] = project
package_dir.update(extra_dirs)
-
+
# applications
packages.extend(apps)
- # with all subpackages
+ # with all subpackages
for app in apps:
packages.extend(find_packages(os.path.join(apps_dir, app), base=app))
# and the project
packages.append(project)
-
- # extra data
+
+ # extra data
extra_data = {}
for app in apps:
- extra_data[app] = files_from(*RESOURCE_PATHS, base=os.path.join(apps_dir, app))
+ extra_data[app] = files_from(*RESOURCE_PATHS, base=os.path.join(apps_dir, app))
extra_data[project] = files_from(*RESOURCE_PATHS, base=project)
- package_data.update(extra_data)
-
+ package_data.update(extra_data)
+
return setup(
- package_dir = package_dir,
- packages = packages,
- package_data = package_data, **kwargs)
-
+ package_dir=package_dir,
+ packages=packages,
+ package_data=package_data, **kwargs)
+
+
#
-# The reald stuff :)
+# The real stuff :)
#
django_setup(
name='fnp-redakcja',
author="Fundacja Nowoczesna Polska",
author_email='fundacja@nowoczesnapolska.org.pl',
license="GNU Affero General Public License 3",
- maintainer='Łukasz Rekucki',
+ maintainer='Łukasz Rekucki',
maintainer_email='lrekucki@gmail.com',
url='http://github.com/fnp/redakcja',
- package_dir = {'': 'lib'},
- py_modules = [
+ package_dir={'': 'lib'},
+ py_modules=[
'wlapi',
- 'vstorage',
- ],
+ 'vstorage',
+ ],
scripts=[
- 'scripts/crop.py',
+ 'scripts/crop.py',
'scripts/imgconv.py',
- ],
+ ],
# django applications
- project = 'platforma',
- apps_dir = 'apps',
- apps = [
+ project='platforma',
+ apps_dir='apps',
+ apps=[
'compress',
- 'django_cas',
+ 'django_cas',
'filebrowser',
'toolbar',
- 'wiki',
+ 'wiki',
],
- requires = [
+ requires=[
"librarian (>=1.3)",
- "Django (>=1.1.1,<1.2)",
- ]
- # data_files=[ ('', ['LICENSE', 'NOTICE', 'README.rst', 'AUTHORS.md', 'requirements.txt'])],
-)
+ "Django (>=1.1.1,<1.2)",
+ ])