Requesting architectural input on calendar module

Topics: Customizing Orchard, Writing modules
Apr 30, 2011 at 8:02 PM

Hello All,

I've been writing a calendar and events module as a learning exercise.  I started by using the Blog module as a reference, and then extended / modified to fit the calendar / event data model.  The module works great now, except that my slug urls are /[calendarname]/[sluggified-calendarevent-title].  I'm more interested in a slug that includes the event start date such as /[calendarname]/[year]/[month]/[day]/[sluggified-calendarevent-title].  With this type of url, I can present lists of events in a much more flexible manner (by year, or month or day) and we'll have less unique title conflicts (especially for recurring meetings, etc.).

I began down the path of looking for interfaces I could override in the Routable module, but then ran across this note by randompete that gave me pause.  I can take this in one of a few ways from this point (that I can see):

  1. Use CalendarPart and CalendarEventPart models and somehow modify the slug / routing to add the date.
  2. Use more of the container / containable model and add container content items for years, months and days under a CalendarPart upon the creation of a CalendarEventPart.  The CalendarEventPart would then live in a day container.
  3. Add the CalendarEvent id into the URL and generate / use routes that will end up ignoring everything except for the id (ie. /2011/04/30/[calendareventid]/[sluggified-calendarevent-title]).  I don't really like this one, but figured I'd add it to the list as a possibility.  I'd probably just drop the use of Routable all together on this.

I'm guessing that the answer to this design decision would apply to a lot of other module implementations (like the Blog for instance), so I figured it would probably be worth a public discussion.  Does anyone have any opinions on the best approach / design pattern for this task?

Thanks all!
-Steve

May 3, 2011 at 2:44 PM

Hi Steve,

There are probably a lot of ways you could approach this.

What I'm thinking might work is:

- Create a RoutableDatePart. This separates the route from the actual content on the end of it, so you could potentially use it for other things (e.g. blogs) and perhaps implement that as a separate module. It might need to implement IRoutableAspect (not sure about this).

- Create Routes.cs and ItemController.cs to set up the routes and actions to handle the year/month/day route. You'll need to set up your own route constraints and dictionary of existing routes as the blog module does.

- Either find a way to leverage Container/Containable to handle the top-level URL, or you might have to just implement your own DateContainer system to hold RoutableDatePart items.

This is quite vague because I'm not 100% sure about all this. It'd certainly be nice to have this kind of routing, and on blogs as well. I'm wondering if the team might be thinking about this for future versions already?

May 3, 2011 at 4:43 PM

Thanks for the reply as I really appreciate your insights.  I went down the route of a RoutableDatePart last week.  I agree that it would be really nice to have not only on this, but on other parts.  I ran into an issue when enabling my new routabledatepart module that stalled me in that effort.  I'm getting a "Sequence contains more than one matching element" exception from line 54 of \src\Orchard\ContentManagement\DefaultContentManager.cs.  It's definitely got to do with my lack of understanding of routing in Orchard, so I need to dig in more to find out what I'm doing wrong.

Was hoping that I could just copy the existing Routable module from core, modify as necessary and enable as a module, but obviously more understanding is necessary.

If anyone is interested in working on this together, I'd be happy to participate and contribute my initial thoughts and code.  In the mean time, I'll keep hacking away.

Best Regards,
-Steve

May 3, 2011 at 4:58 PM

There are some kind of similar route issues I need to look at. Basically I've been building this connections system that allows arbitrary n:n connections between content. I want to be able to create my own route hierarchy using particular connection types.

I'd started just by duplicating blog routing like you but yeah it looked really complicated and I put it to one side for the time being. But it'd be a good thing to start looking at again. Have you got your code somewhere I can have a look?

May 3, 2011 at 5:20 PM

I just got over the exceptions that I was running into (copy and paste mistakes plus removed the "promote to homepage" feature for now).  Let me see if I can clean the module up tonight and I'll post it somewhere you can get to it tomorrow for review.  I don't want to waste your time sweating the small stuff.

Thanks!

May 4, 2011 at 3:26 AM
Edited May 4, 2011 at 5:11 AM

I've got a first draft of the RoutableByDate module ready for review.  The code has not been optimized, refactored or tested to any level near production use.  I've tested it with both my calendar module and the stock blogs module.  To get the blog module to use it from a fresh Orchard site:

1.) Download a copy of my module from http://slamm.com/temp/slamm.orchard.routablebydate.zip
2.) Copy this module (expanded from the zip) to your modules directory.
3.) NOT NECESSARY: Add a hard project dependency to Slamm.Orchard.RoutableByDate in the Blog module project.
4.) Add a dependency to Slamm.Orchard.RoutableByDate in the blog module module.txt file.
5.) Modify the blog module's migration.cs file to include the RouteByDatePart instead of the RoutePart in the BlogPost type definition.
6.) Create a new site with the default recipe so that the blog module runs its migration and enables the RoutableByDate module.

Now, when you create a new blog post, you'll see a Route Date field under the Title.  This field expects a date (in the M/d/yyyy format).  I didn't put any validation or anything in yet on this field.  Once you fill out the title and the Route date, you should see the slug update to /yyyy/M/d/slugified-post-title.

I don't believe I modified anything else to make this work with the rest of the blog code and it should work transparently in the site.

As most developers would utilize this part to use an existing date of their model for the routing date, my plan is to hide the Route Date field with css and then utilize javascript specific to the module i'm working on to move a date from another model field into the Route Date field.  For instance, I will wire up the EventStartDate from my calendar event model to populate the Route Date field on blur of the EventStartDate field in the editor.

I'm currently using .blur from jQuery to kick off the slug generation leaving the Route Date field, so if it's hidden, i'll have to make sure i can still kick off that event programatically or utilize another mechanism.  Also, I thought about using a hidden HTML input for the Route Date field, but thought that would reduce the flexibility of the module.

I think I'm really happy with this solution as it required minimal changes to my existing Calendar module and the blog module to make it work and I haven't stumbled across any side effects just yet.  I'd love to hear the community's thoughts on if I'm heading in a direction that would be useful to others.  Any suggestions are also welcome.  When / if we get it in an acceptable form, I'd be happy to submit it to the gallery.

Best Regards,
-Steve

May 4, 2011 at 5:10 AM

My bad, skip step #3 above.  Not necessary.

May 4, 2011 at 8:35 AM

Good work, looks really neat!

I'm just thinking about the "routable date" issue. There were some discussions elsewhere about a problem with the "published date" in that it gets changed every time you edit something and there's no way to set it to a specified value. Obviously you don't want the actual Url of an item changing after you've published it (broken links etc.)  So for this reason alone it's good to not rely on the published date (although it'd be nice if you could populate from that or even just DateTime.Now if no date is specified).

I'm wondering if something can be done server-side to maintain a flexible way of sourcing the date to be used; so for instance you could provide an IDateAspect interface; the RoutableByDatePart will look for any parts with IDateAspect and get the date from them (or use DateTime.Now otherwise). So this would let you grab a date from EventPart, and actually factor the current date input out to a "DisplayDate" part. So the routing and the date input can be used independently.

I should let you know about one potential gotcha with the way RoutePart usually generates slugs. When I wrote the ContentNotifications module I noticed that slug generation actually triggers a Publish event for a "fake" item and all its content parts; the problem is this "fake" publish event is indistinguishable from a real one! So ContentNotifications ends up sending out two emails for everything that gets published. I don't know the best way around this as it seems to be a necessary part of generating the slug (but I'm hoping this gets changed in Orchard at some point)

Just one further suggestion with the instructions you wrote for adding to blog module. I'd avoid recommending changes to any Orchard modules (it could leave someone in problems much much later on if Orchard gets updated and any migrations are added to that module).

You could actually include an extra migration with your module as a separate feature. All it would do is modify the BlogPost content type to add the RouteByDatePart and remove the RoutePart. It'd probably be a good idea to copy all titles and slugs over from RoutePart at the same time tho ;)

May 4, 2011 at 1:08 PM

Great feedback, thanks.  The point about adding something like IDateAspect is intriguing.  The mental gap I'm trying to jump now is if we're doing that server side, how would I wire up the "fake" post automatically to generate the slug before the item is published (or the editor form is completed and submitted).  The editor form would need to have JavaScript injected in that adds an event to the IDateAspect field to call the slugify code.  This JavaScript logic is hard coded in the view now as I know which field I need to watch ahead of time.  I'll need to think about that a bit.

I hadn't thought about automating the use of the RoutableByDatePart for the blog.  Great idea for a real module feature.  I guess that could help some folks out who aren't interested in getting into the code.  Do you think a one-way migration would be acceptable:

  1. Enable the Routable By Date feature and the Routable By Date For Blogs feature.
  2. Migration runs to add RoutableByDatePart to BlogPost.
  3. Migration copies all existing RoutablePart titles to RoutableByDatePart titles.
  4. Migration uses published date (falls back to created date) to populate RoutableByDatePart Date field.
  5. Migration generates slugs in each new RoutableByDatePart.
  6. Migration removes RoutablePart.

I'm not sure if we could hook the disabling of a feature, so this would be a one way move for now.  Does this make sense?  Not saying I have any idea how to do what I've just stated, but I'll get to hacking (I mean learning) if it is a reasonable approach.

Thanks!

May 4, 2011 at 1:20 PM

I think you still need RoutableByDatePart to hook in the routing and slug generation. But it can go off and look for a part with IDateAspect to determine the date to use. Just makes it really easy to plug in different date logic.

I think a one-way migration is fine, to be honest all migrations are one way. I'm not even sure if removing RoutablePart even deletes the original routepart records. Also you could put a note in the feature description that makes it clear this is what's happening. Anyway users should always backup their database before installing or updating any features, I'm sure you and I both do that right? ;)

Another way you could do it is have a command-line command to copy the titles and have a recipe that will do all of the above.