1
votes

I have a content part that provides a begin timestamp and end timestamp option. These 2 fields are used to define a period of time in which the content item should be displayed.

I now have difficulties to implement a skip approach whereas content items should not be displayed / skipped when the period of time does not span the current time.

Digging in the source code and trying to find an entry point for my approach resulted in the following content handler

public class SkipContentHandler : Orchard.ContentManagement.Handlers.ContentHandler
{
  protected override void BuildDisplayShape(Orchard.ContentManagement.Handlers.BuildDisplayContext aContext)
  {
    if (...) // my condition to process only content shapes which need to be skipped
    {
      aContext.Shape = null; // return null shape to skip it
    }
  }
}

This works but there are several side effects

  • I had to alter the source code of BuildDisplayContext as the Shape is normally read only
  • List shape may displayed a wrong pager when it contains content items with my content part because the Count() call in ContainerPartDriver.Display() is executed before BuildDisplay()
  • calling the URL of a content item that is skipped results in an exception because View(null) is abigious

So, what would be the correct approach here or is there any module in existence that does the job? I couldn't find one.

1
You might want to consider to just instead register an 'unpublish' event when the time span expiresdevqon
@devqon Yeah i thought about that too. But this would require some sort of background task that runs periodically and this task needs to query all content items to search for the right ones to unpublish. Also, i'm not a fan of unpublishing as it might confuse the user which had clicked the publish button before. I still hope that there is a way to abort the rendering of a shape safely somehow...ViRuSTriNiTy
You might succeed in stop it from rendering, but then you will still have problems with the Count as you mentioned. So paging probably fails, and probably more with thatdevqon
If you cannot or don't want to alter the queries that return your content, you could create a module that provides fields for the timestamps you mentioned as an attachable part, e.g. DisplayDurationPart. Then I'd create a custom ContentQuery class that inherits from DefaultContentQuery and, for example, alter the .List()-Method to filter for that fields before calling Slice(). After that you would need to register the custom ContentQuery class with autofac; check Orchard.ContentManagement.ContentModule. If you're interested in that approach I could create a small demo.Xceno
@ViRuSTriNiTy I just got stuck with your exact same problem some minutes ago. I'll keep you updated when I've found a definitive solution. But damn you're right on the DefaultContentQuery; seems like you would have to implement the interface yourself (and just c/p from the standard implementation... meh)Xceno

1 Answers

1
votes

This is a quite complex task. There are several steps needed to achieve a proper skipping of display items:

  1. Create the part correctly

    There are a few pitfalls here as when coming to the task of adding a part view one might utilize Orchards date time editor in connection with the DateTime properties. But this brings a heck of a lot of additional issues to the table but these don't really relate to the question.

    If someone is interested in how to use Orchards date time editor then i can post this code too, but for now it would only blow up the code unnecessarly.

    So here we go, the part class...

    public class ValidityPart : Orchard.ContentManagement.ContentPart<ValidityPartRecord>
    {
      // public
        public System.DateTime? ValidFromUtc 
        { 
          get { return Retrieve(r => r.ValidFromUtc); }
          set { Store(r => r.ValidFromUtc, value); }
        }
    
        ...
    
        public System.DateTime? ValidTillUtc 
        { 
          get { return Retrieve(r => r.ValidTillUtc); }
          set { Store(r => r.ValidTillUtc, value); }
        }
    
        ...
    
        public bool IsContentItemValid()
        {
          var lUtcNow = System.DateTime.UtcNow;
    
          return (ValidFromUtc == null || ValidFromUtc.Value <= lUtcNow) && (ValidTillUtc == null || ValidTillUtc.Value >= lUtcNow);
        }
    
      ...
    }
    

    ...and the record class...

    public class ValidityPartRecord : Orchard.ContentManagement.Records.ContentPartRecord
    {
      // valid from value as UTC to use Orchard convention (see CommonPart table) and to be compatible with projections
      // (date/time tokens work with UTC values, see https://github.com/OrchardCMS/Orchard/issues/6963 for a related issue)  
      public virtual System.DateTime? ValidFromUtc { get; set; }
    
      // valid from value as UTC to use Orchard convention (see CommonPart table) and to be compatible with projections
      // (date/time tokens work with UTC values, see https://github.com/OrchardCMS/Orchard/issues/6963 for a related issue)  
      public virtual System.DateTime? ValidTillUtc { get; set; }
    }
    
  2. Create a customized content query class

    public class MyContentQuery : Orchard.ContentManagement.DefaultContentQuery
    { 
      // public
        public ContentQuery(Orchard.ContentManagement.IContentManager aContentManager, 
          Orchard.Data.ITransactionManager aTransactionManager, 
          Orchard.Caching.ICacheManager aCacheManager,
          Orchard.Caching.ISignals aSignals,
          Orchard.Data.IRepository<Orchard.ContentManagement.Records.ContentTypeRecord> aContentTypeRepository,
          Orchard.IWorkContextAccessor aWorkContextAccessor) 
            : base(aContentManager, aTransactionManager, aCacheManager, aSignals, aContentTypeRepository)
        {
          mWorkContextAccessor = aWorkContextAccessor;
        }
    
        protected override void BeforeExecuteQuery(NHibernate.ICriteria aContentItemVersionCriteria)
        {
          base.BeforeExecuteQuery(aContentItemVersionCriteria);
    
          // note:
          //  this method will be called each time a query for multiple items is going to be executed (e.g. content items of a container, layers, menus),
          //  this gives us the chance to add a validity criteria
    
          var lWorkContext = mWorkContextAccessor.GetContext();
    
          // exclude admin as content items should still be displayed / accessible when invalid as validity needs to be editable
          if (lWorkContext == null || !Orchard.UI.Admin.AdminFilter.IsApplied(lWorkContext.HttpContext.Request.RequestContext))
          {
            var lUtcNow = System.DateTime.UtcNow;
    
            // left outer join of ValidityPartRecord table as part is optional (not present on all content types)
            var ValidityPartRecordCriteria = aContentItemVersionCriteria.CreateCriteria(
              "ContentItemRecord.ValidityPartRecord", // string adopted from foreach loops in Orchard.ContentManagement.DefaultContentQuery.WithQueryHints()
              NHibernate.SqlCommand.JoinType.LeftOuterJoin 
            );
    
            // add validity criterion
            ValidityPartRecordCriteria.Add( 
              NHibernate.Criterion.Restrictions.And(
                NHibernate.Criterion.Restrictions.Or(
                  NHibernate.Criterion.Restrictions.IsNull("ValidFromUtc"),
                  NHibernate.Criterion.Restrictions.Le("ValidFromUtc", lUtcNow)
                ),
                NHibernate.Criterion.Restrictions.Or(
                  NHibernate.Criterion.Restrictions.IsNull("ValidTillUtc"),
                  NHibernate.Criterion.Restrictions.Ge("ValidTillUtc", lUtcNow)
                )
              )
            );
          }
        }
    
      // private
        Orchard.IWorkContextAccessor mWorkContextAccessor;
    }
    

    This essentially adds a left join of the validity part fields to the SQL query (content query) and extends the WHERE statement with the validity condition.

    Please note that this step is only possible with the solution described the following issue: https://github.com/OrchardCMS/Orchard/issues/6978

  3. Register the content query class

    public class ContentModule : Autofac.Module
    {
      protected override void Load(Autofac.ContainerBuilder aBuilder)
      {
        aBuilder.RegisterType<MyContentQuery>().As<Orchard.ContentManagement.IContentQuery>().InstancePerDependency();
      }
    }
    
  4. Create a customized content manager

    public class ContentManager : Orchard.ContentManagement.DefaultContentManager
    {
      // public
        public ContentManager(
          Autofac.IComponentContext aContext,
          Orchard.Data.IRepository<Orchard.ContentManagement.Records.ContentTypeRecord> aContentTypeRepository,
          Orchard.Data.IRepository<Orchard.ContentManagement.Records.ContentItemRecord> aContentItemRepository,
          Orchard.Data.IRepository<Orchard.ContentManagement.Records.ContentItemVersionRecord> aContentItemVersionRepository,
          Orchard.ContentManagement.MetaData.IContentDefinitionManager aContentDefinitionManager,
          Orchard.Caching.ICacheManager aCacheManager,
          System.Func<Orchard.ContentManagement.IContentManagerSession> aContentManagerSession,
          System.Lazy<Orchard.ContentManagement.IContentDisplay> aContentDisplay,
          System.Lazy<Orchard.Data.ITransactionManager> aTransactionManager,
          System.Lazy<System.Collections.Generic.IEnumerable<Orchard.ContentManagement.Handlers.IContentHandler>> aHandlers,
          System.Lazy<System.Collections.Generic.IEnumerable<Orchard.ContentManagement.IIdentityResolverSelector>> aIdentityResolverSelectors,
          System.Lazy<System.Collections.Generic.IEnumerable<Orchard.Data.Providers.ISqlStatementProvider>> aSqlStatementProviders,
          Orchard.Environment.Configuration.ShellSettings aShellSettings,
          Orchard.Caching.ISignals aSignals,
          Orchard.IWorkContextAccessor aWorkContextAccessor)
            : base(aContext, aContentTypeRepository, aContentItemRepository, aContentItemVersionRepository, aContentDefinitionManager, aCacheManager, aContentManagerSession,
                aContentDisplay, aTransactionManager, aHandlers, aIdentityResolverSelectors, aSqlStatementProviders, aShellSettings, aSignals)
        {
          mWorkContextAccessor = aWorkContextAccessor;
        }
    
        public override ContentItem Get(int aId, Orchard.ContentManagement.VersionOptions aOptions, Orchard.ContentManagement.QueryHints aHints)
        {
          var lResult = base.Get(aId, aOptions, aHints);
    
          if (lResult != null)
          {
            // note:
            //  the validity check is done here (after the query has been executed!) as changing base.GetManyImplementation() to 
            //  apply the validity critera directly to the query (like in ContentQuery) will not work due to a second attempt to retrieve the
            //  content item from IRepository<> (see base.GetManyImplementation(), comment "check in memory") when the query
            //  returns no data (and the query should not return data when the validity critera is false)
            //
            // http://stackoverflow.com/q/37841249/3936440
    
            var lWorkContext = mWorkContextAccessor.GetContext();
    
            // exclude admin as content items should still be displayed / accessible when invalid as validity needs to be editable
            if (lWorkContext == null || !Orchard.UI.Admin.AdminFilter.IsApplied(lWorkContext.HttpContext.Request.RequestContext))
            {
              var lValidityPart = lResult.As<ValidityPart>();
              if (lValidityPart != null)
              {
                if (lValidityPart.IsContentItemValid())
                {
                  // content item is valid
                }
                else
                {
                  // content item is not valid, return null (adopted from base.Get())
    
                  lResult = null;
                }
              }
            }
          }
    
          return lResult;
        }
    
      // private
        Orchard.IWorkContextAccessor mWorkContextAccessor;
    }
    

Steps 2-4 are needed when having content items whereas the content type has a Container and Containable part or even content items which are processed / displayed separately. Here you normally cannot customize the content query that is executed behind the scenes.

Steps 2-4 are not needed if you use the Projection module. But again, this brings a few other issues to the table as reported in this issue: https://github.com/OrchardCMS/Orchard/issues/6979