Changeset 3906

Show
Ignore:
Timestamp:
06/26/08 12:29:39 (5 months ago)
Author:
osimons
Message:

FullBlogPlugin: Major refactoring of the post naming to make it more flexible.

  • Now supports any name that don't conflict with the few needed plugin 'commands' such as /edit/, /author/someone, /year/month etc.
  • Also supports '/' in names for easier migration from wiki-based posts with migration script, and also allows flexible naming often requested.
  • New setting for default_postname that also supports Python time string formatting substitution as well as the $USER variable. Setting also available in admin page.
  • Refactored to make the path parsing (command + pagename) easier to comprehend and maintain. Similar treatment to the new post name checking.
  • Improved the warning and notice logic for the user to provide relevant messages for all create situations I could think of.
  • Added a notice (sidebar) on post naming that may just be over the top in hand-holding. Let me know what you think - happy to remove that one again.

Changes should close tickets #2460 and #2956.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • fullblogplugin/0.11/tracfullblog/admin.py

    r3131 r3906  
    3939                self.env.config.set('fullblog', 'num_items_front', 
    4040                    req.args.get('numpostsfront')) 
     41                self.env.config.set('fullblog', 'default_postname', 
     42                    req.args.get('defaultpostname')) 
    4143                self.env.config.save() 
    4244            elif req.args.get('savebloginfotext'): 
     
    5557        blog_admin['numpostsfront'] = self.env.config.getint( 
    5658                                            'fullblog', 'num_items_front') 
     59        blog_admin['defaultpostname'] = self.env.config.get( 
     60                                            'fullblog', 'default_postname') 
    5761         
    5862        return ('fullblog_admin.html', {'blog_admin': blog_admin}) 
  • fullblogplugin/0.11/tracfullblog/core.py

    r3284 r3906  
    1111""" 
    1212 
     13from time import strftime 
     14 
    1315from genshi.builder import tag 
    1416 
    1517from trac.attachment import ILegacyAttachmentPolicyDelegate 
    1618from trac.core import * 
    17 from trac.config import IntOption 
     19from trac.config import Option 
    1820from trac.perm import IPermissionRequestor 
    1921from trac.resource import IResourceManager 
    2022from trac.util.text import unicode_unquote 
     23from trac.util.datefmt import to_datetime, utc 
    2124from trac.wiki.api import IWikiSyntaxProvider 
    2225 
     
    2427from api import IBlogChangeListener, IBlogManipulator 
    2528from model import BlogPost, get_blog_resources 
    26  
     29from util import parse_period 
    2730 
    2831class FullBlogCore(Component): 
     
    3538    manipulators = ExtensionPoint(IBlogManipulator) 
    3639     
     40    implements(IPermissionRequestor, IWikiSyntaxProvider, IResourceManager, 
     41            ILegacyAttachmentPolicyDelegate) 
     42 
    3743    # Options 
    38      
    39     IntOption('fullblog', 'num_items_front', 20, 
    40         """Option to specify how many recent posts to display on the 
    41         front page of the Blog.""") 
    42  
    43     default_pagename = 'change_this_post_shortname' 
     44 
     45    Option('fullblog', 'default_postname', '%Y/%m/%d/my_topic', 
     46        """Option for a default naming scheme for new posts. The string 
     47        can include substitution markers for time (UTC) and user: %Y=year, 
     48        %m=month, %d=day, %H=hour, %M=minute, %S=second, $USER. 
     49        Example template string: `%Y/%m/%d/my_topic`""") 
     50 
     51    # Constants 
     52 
    4453    reserved_names = ['create', 'view', 'edit', 'delete', 
    4554                    'archive', 'category', 'author'] 
    46      
    47     implements(IPermissionRequestor, IWikiSyntaxProvider, IResourceManager, 
    48             ILegacyAttachmentPolicyDelegate) 
    4955 
    5056    # IPermissionRequestor method 
     
    189195        # Do basic checking for content existence 
    190196        warnings.extend(bp.save(version_author, version_comment, verify_only=True)) 
    191         # Do some more fundamental checking 
    192         if bp.name in self.reserved_names: 
    193             warnings.append((req, "'%s' is a reserved name. Please change." % bp.name)) 
    194         if bp.name == self.default_pagename: 
    195             warnings.append(('post_name', "The default page shortname must be changed.")) 
     197        # Make sure name for the post is a valid name 
     198        warnings.extend(self._check_new_postname(req, bp.name)) 
    196199        # Check if any plugins has objections with the contents 
    197200        fields = { 
     
    278281            warnings.append(('', "Unknown error. Not deleted.")) 
    279282        return warnings 
     283 
     284    # Internal methods 
     285     
     286    def _get_default_postname(self, user=''): 
     287        """ Parses and returns the setting for default_postname. """ 
     288        opt = self.env.config.get('fullblog', 'default_postname') 
     289        if not opt: 
     290            return '' 
     291        # Perform substitutions 
     292        try: 
     293            now = to_datetime(None, utc).timetuple() 
     294            name = strftime(opt, now) 
     295            name = name.replace('$USER', user) 
     296            return name 
     297        except: 
     298            self.env.log.debug( 
     299                "FullBlog: Error parsing default_postname option: %s" % opt) 
     300            return '' 
     301 
     302    def _check_new_postname(self, req, name): 
     303        """ Does some checking on the postname to make sure it does 
     304        not conflict with existing commands. """ 
     305        warnings = [] 
     306        name = name.lower() 
     307        # Reserved names 
     308        for rn in self.reserved_names: 
     309            if name == rn: 
     310                warnings.append(('', 
     311                    "'%s' is a reserved name. Please change." % name)) 
     312            if name.startswith(rn + '/'): 
     313                warnings.append(('', 
     314                    "Name cannot start with a reserved name as first item in " 
     315                    "path ('%s'). Please change." % rn)) 
     316        # Check to see if it is a date range 
     317        items = name.split('/') 
     318        if len(items) == 2 and parse_period(items) != (None, None): 
     319            warnings.append(('', 
     320                "'%s' is seen as a time period, and cannot " 
     321                "be used as a name. Please change." % name))         
     322        return warnings 
  • fullblogplugin/0.11/tracfullblog/templates/fullblog_admin.html

    r2710 r3906  
    2222        <legend>Blog Settings:</legend> 
    2323        <div class="field"> 
    24          <label for="numpostsfront">Number of posts on front page: 
    25            <input type="text" name="numpostsfront" 
     24         <label for="numpostsfront">Number of posts on front page:<br /> 
     25           <input type="text" size="10" name="numpostsfront" 
    2626                              value="${blog_admin.numpostsfront}" /> 
     27         </label> 
     28        </div> 
     29        <div class="field"> 
     30         <label for="defaultpostname">Template for naming new blog posts:<br /> 
     31                (username and time substitution available, like <tt>${'$USER'}-%Y/%m/%d/my_topic</tt>)<br /> 
     32           <input type="text" size="35" name="defaultpostname" 
     33                              value="${blog_admin.defaultpostname}" /> 
    2734         </label> 
    2835        </div> 
  • fullblogplugin/0.11/tracfullblog/templates/fullblog_edit.html

    r2928 r3906  
    1717    <div id="content" class="blog wiki"> 
    1818       
     19    <py:with vars="is_create = not blog_edit.version; 
     20                   can_create = 'BLOG_CREATE' in perm('blog'); 
     21                   is_edit = bool(blog_edit.version); 
     22                   can_edit = 'BLOG_MODIFY_ALL' in perm(blog_edit.resource) or defined( 
     23                        'blog_orig_author') or ( 
     24                        perm.username==blog_edit.author and 'BLOG_MODIFY_OWN' in perm(blog_edit.resource)); 
     25                   is_allowed = (is_create and can_create) or (is_edit and can_edit); 
     26                  "> 
     27 
     28      <div py:if="is_create and can_create" id="sidebar"> 
     29        <p><em>Naming your blog posts.</em></p> 
     30        <p>When naming your posts (post shortname), it is recommended to make them URL friendly 
     31          (for instance avoid spaces).</p> 
     32        <p>Blog posts can also be referenced with <tt>[blog:postname]</tt> wiki link syntax, 
     33          so you may not want to make them too hard to read or write.</p> 
     34        <p>Some names are reserved and not allowed:</p> 
     35        <ul> 
     36          <li>'view', 'create', 'edit', 'archive', 'delete', 'category', 'author' - 
     37            either on their own or as first item in a path.</li> 
     38          <li>Numbers evaluating to N/M are seen as time periods and 
     39            used for month-based browsing.</li> 
     40        </ul> 
     41      </div> 
     42 
    1943      <div id="main"> 
    2044       
     
    2246        <h1 py:if="blog_edit.version">Edit Blog Post</h1> 
    2347 
    24         <py:with vars="is_create = not blog_edit.version; 
    25                        can_create = 'BLOG_CREATE' in perm('blog'); 
    26                        is_edit = bool(blog_edit.version); 
    27                        can_edit = 'BLOG_MODIFY_ALL' in perm(blog_edit.resource) or defined( 
    28                             'blog_orig_author') or ( 
    29                             perm.username==blog_edit.author and 'BLOG_MODIFY_OWN' in perm(blog_edit.resource)); 
    30                        is_allowed = (is_create and can_create) or (is_edit and can_edit); 
    31                       "> 
    3248          <div py:if="not is_allowed" class="system-message">You do not have the required permission to 
    3349              ${is_create and 'create a blog post' or 'edit this blog post'}.</div> 
     
    101117            </fieldset> 
    102118          </form> 
    103         </py:with> 
    104119 
    105120      </div> 
    106121 
     122    </py:with> 
    107123    </div> 
    108124 
  • fullblogplugin/0.11/tracfullblog/util.py

    r3695 r3906  
    3333    """ Parses a list of items for elements of dates, and returns 
    3434    a month as (from_dt, to_dt) if valid. (None, None) if not. """ 
    35     if not len(items) >= 2: 
     35    if not len(items) == 2: 
    3636        return None, None 
    3737    try: 
  • fullblogplugin/0.11/tracfullblog/web_ui.py

    r3892 r3906  
    1313import re 
    1414from pkg_resources import resource_filename 
    15 from trac.util.compat import itemgetter 
    1615 
    1716# Trac and Genshi imports 
    1817from genshi.builder import tag 
    1918from trac.attachment import AttachmentModule 
    20 from trac.config import ListOption, BoolOption 
     19from trac.config import ListOption, BoolOption, IntOption 
    2120from trac.core import * 
    2221from trac.mimeview.api import Context 
     
    2423from trac.search.api import ISearchSource, shorten_result 
    2524from trac.timeline.api import ITimelineEventProvider 
    26 from trac.util.compat import sorted 
     25from trac.util.compat import sorted, itemgetter 
    2726from trac.util.datefmt import utc 
    2827from trac.util.translation import _ 
     
    4645               ITemplateProvider) 
    4746 
     47    # Options 
     48     
    4849    ListOption('fullblog', 'month_names', 
    4950        doc = """Ability to specify a list of month names for display in groupings. 
     
    5152        Enter list of 12 months like: 
    5253        `month_names = January, February, ..., December` """) 
     54 
    5355    BoolOption('fullblog', 'personal_blog', False, 
    5456        """When using the Blog as a personal blog (only one author), setting to 'True' 
    5557        will disable the display of 'Browse by author:' in sidebar, and also removes 
    5658        various author links and references. """) 
     59 
     60    IntOption('fullblog', 'num_items_front', 20, 
     61        """Option to specify how many recent posts to display on the 
     62        front page of the Blog.""") 
    5763 
    5864    # INavigationContributor methods 
     
    9096 
    9197        blog_core = FullBlogCore(self.env) 
    92         default_pagename = blog_core.default_pagename 
    93         reserved_names = blog_core.reserved_names 
    94  
    9598        format = req.args.get('format', '').lower() 
    96          
    97         # Parse out the path and actions from args 
    98         path_items = req.args.get('blog_path', '').split('/') 
    99         path_items = [item for item in path_items if item] # clean out empties 
     99 
     100        command, pagename, path_items, listing_data = self._parse_path(req) 
    100101        action = req.args.get('action', 'view').lower() 
    101102        try: 
     
    103104        except: 
    104105            version = 0 
    105         command = pagename = '' 
    106         command = (len(path_items) and path_items[0]) or '' 
    107         if command.lower() in [u'view', u'edit', 'delete'] and len(path_items) == 2: 
    108             pagename = path_items[1] 
    109         if command and command not in [ 
    110                 'view', 'edit', 'create', 'archive', 'delete']: 
    111             if len(path_items) == 1: 
    112                 # Assume it is a request for a specific post 
    113                 pagename = command 
    114                 command = 'view' 
    115             else: 
    116                 # Assume it is a listing, do further parsing later 
    117                 command = 'listing' 
    118106 
    119107        data = {} 
    120  
    121108        template = 'fullblog_view.html' 
    122109        data['blog_about'] = BlogPost(self.env, 'about') 
     
    202189        elif command in ['create', 'edit']: 
    203190            template = 'fullblog_edit.html' 
    204             pagename = pagename or req.args.get('name','') or default_pagename 
    205             the_post = BlogPost(self.env, pagename) 
     191            default_pagename = blog_core._get_default_postname(req.authname) 
     192            the_post = BlogPost(self.env, pagename or default_pagename) 
     193            warnings = [] 
     194 
     195            if command == 'create' and the_post.version: 
     196                if 'BLOG_CREATE' in req.perm and the_post.name == default_pagename \ 
     197                                    and not req.method == 'POST': 
     198                    if default_pagename: 
     199                        add_notice(req, "Suggestion for new name already exists " 
     200                            "('%s'). Please make a new name." % the_post.name) 
     201                elif pagename: 
     202                    warnings.append( 
     203                        ('', "A post named '%s' already exists. Enter new name." 
     204                                            % the_post.name)) 
     205                the_post = BlogPost(self.env, '') 
    206206            if command == 'edit': 
    207207                req.perm(the_post.resource).require('BLOG_VIEW') # Starting point 
     
    220220                    else: 
    221221                        req.perm(the_post.resource).require('BLOG_MODIFY_ALL') 
    222                 # Input verifications and warnings 
    223                 warnings = [] 
    224                 if command == 'create' and the_post.version: 
    225                     warnings.append( 
    226                             ('', "A post named '%s' already exists. Reverting to default name." 
    227                                             % the_post.name)) 
    228                     the_post = BlogPost(self.env, default_pagename) 
     222 
     223                # Check input 
    229224                orig_author = the_post.author 
    230225                if not the_post.update_fields(req.args): 
     
    249244                        "edit the post again due to restricted permissions.") 
    250245                    data['blog_orig_author'] = orig_author 
    251                 for field, reason in warnings: 
    252                     if field: 
    253                         add_warning(req, "Field '%s': %s" % (field, reason)) 
    254                     else: 
    255                         add_warning(req, reason)                         
     246            for field, reason in warnings: 
     247                if field: 
     248                    add_warning(req, "Field '%s': %s" % (field, reason)) 
     249                else: 
     250                    add_warning(req, reason) 
    256251            data['blog_edit'] = the_post 
    257252 
     
    301296                    add_warning(req, reason)                         
    302297 
    303         elif command == 'listing'
     298        elif command.startswith('listing-')
    304299            # 2007/10 or category/something or author/theuser 
    305300            title = category = author = '' 
    306             from_dt, to_dt = parse_period(path_items) 
    307             if from_dt: 
     301            from_dt = to_dt = None 
     302            if command == 'listing-month': 
     303                from_dt = listing_data['from_dt'] 
     304                to_dt = listing_data['to_dt'] 
    308305                title = "Posts for the month of %s %d" % ( 
    309306                        blog_month_names[from_dt.month -1], from_dt.year) 
     
    311308                        'application/rss+xml', 'rss') 
    312309 
    313             category = (path_items[0].lower() == 'category' 
    314                         and path_items[1]) or '' 
    315             if category: 
    316                 title = "Posts in category %s" % category 
    317                 add_link(req, 'alternate', req.href.blog('category', category, format='rss')
    318                     'RSS Feed', 'application/rss+xml', 'rss') 
    319             author = (path_items[0].lower() == 'author' 
    320                         and path_items[1]) or '' 
    321             if author: 
    322                 title = "Posts by author %s" % author 
    323                 add_link(req, 'alternate', req.href.blog('author', author, format='rss')
    324                     'RSS Feed', 'application/rss+xml', 'rss') 
     310            elif command == 'listing-category': 
     311                category = listing_data['category'] 
     312                if category: 
     313                    title = "Posts in category %s" % category 
     314                    add_link(req, 'alternate', req.href.blog('category', category
     315                        format='rss'), 'RSS Feed', 'application/rss+xml', 'rss') 
     316            elif command == 'listing-author': 
     317                author = listing_data['author'] 
     318                if author: 
     319                    title = "Posts by author %s" % author 
     320                    add_link(req, 'alternate', req.href.blog('author', author
     321                        format='rss'), 'RSS Feed', 'application/rss+xml', 'rss') 
    325322            if not (author or category or (from_dt and to_dt)): 
    326323                raise HTTPNotFound("Not a valid path for viewing blog posts.") 
     
    336333            raise HTTPNotFound("Not a valid blog path.") 
    337334 
    338         if (not command or command == 'listing') and format == 'rss': 
     335        if (not command or command.startswith('listing-')) and format == 'rss': 
    339336            data['context'] = Context.from_request(req, absurls=True) 
    340337            return 'fullblog.rss', data, 'application/rss+xml' 
     
    467464        """ Location of Trac templates provided by plugin. """ 
    468465        return [resource_filename('tracfullblog', 'templates')] 
     466 
     467    # Internal methods 
     468 
     469    def _parse_path(self, req): 
     470        """ Parses the request path for the blog and returns a 
     471        ('command', 'pagename', 'path_items', 'listing_data') tuple. """ 
     472        # Parse out the path and actions from args 
     473        path = req.args.get('blog_path', '') 
     474        path_items = path.split('/') 
     475        path_items = [item for item in path_items if item] # clean out empties 
     476        command = pagename = '' 
     477        listing_data = {} 
     478        from_dt, to_dt = parse_period(path_items) 
     479        if not path_items: 
     480            pass # emtpy default for return is fine 
     481        elif len(path_items) > 1 and path_items[0].lower() in ['view', 'edit', 'delete']: 
     482            command = path_items[0].lower() 
     483            pagename = '/'.join(path_items[1:]) 
     484        elif len(path_items) == 1 and path_items[0].lower() == 'archive': 
     485            command = path_items[0].lower() 
     486        elif len(path_items) >= 1 and path_items[0].lower() == 'create': 
     487            command = path_items[0].lower() 
     488            pagename = req.args.get('name','') or (len(path_items) > 1 \ 
     489                                                    and '/'.join(path_items[1:])) 
     490        elif len(path_items) > 1 and path_items[0].lower() in ['author', 'category']: 
     491            command = 'listing' + '-' + path_items[0].lower() 
     492            listing_data[path_items[0].lower()] = '/'.join(path_items[1:]) 
     493        elif len(path_items) == 2 and (from_dt, to_dt) != (None, None): 
     494            command = 'listing-month' 
     495            listing_data['from_dt'] = from_dt 
     496            listing_data['to_dt'] = to_dt 
     497        else: 
     498            # A request for a regular page 
     499            command = 'view' 
     500            pagename = path 
     501        return (command, pagename, path_items, listing_data)