Skip to content

This page intentionally left ugly

A programmer and technology enthusiast destroys programming and technology. Welcome to the dichotomy of my existence...

Feel free to browse the experiments and pick up anything you may find useful. Or head over to the obligatory introduction.

WARNING: I post a lot of code on this blog and some of it gets mangled by WordPress formatting. Please double-check for missing or extra quotes, backslashes, '<' and '>' transformed into '&lt;' and '&gt;' and other problems. All the code posted here has been verified to work before I post, except in cases where I explicitly mention that it's incomplete.

Discussion Forum update (Utilities and PostRepository)

February 3, 2012

This is just a followup with two classes from the discussion forum. I haven’t tested the PostRepository class well yet, but I’ll update it with fixes later. Util is the general utilities class that I’ve used in previous projects. It’s basically for rudimentary formatting, input validation etc…

6:40 AM… Time for bed!

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Security.Cryptography;
using System.Globalization;
using System.Web;

namespace Road.Helpers
{
	public class Util
	{
		/// <summary>
		/// Gets the checksum of a local file or some text.
		/// </summary>
		/// <param name="source">Path to a file or a string</param>
		/// <param name="mode">Checksum mode in sha1, sha256, sha512 or md5 (default)</param>
		/// <param name="isFile">True if file mode or false for text mode (defaults to false)</param>
		/// <returns>Completed checksum</returns>
		public static string GetChecksum(string source, string mode = "md5", bool isFile = false)
		{
			byte[] bytes = { };
			Stream fs;

			if (isFile)
				fs = new BufferedStream(File.OpenRead(source), 120000);
			else
				fs = new MemoryStream(Encoding.UTF8.GetBytes(source));

			switch (mode.ToLower())
			{
				case "sha1":
					using (SHA1CryptoServiceProvider sha1 =
						new SHA1CryptoServiceProvider())
						bytes = sha1.ComputeHash(fs);
					break;

				case "sha256":
					using (SHA256CryptoServiceProvider sha256 =
						new SHA256CryptoServiceProvider())
						bytes = sha256.ComputeHash(fs);
					break;

				case "sha512":
					using (SHA512CryptoServiceProvider sha512 =
						new SHA512CryptoServiceProvider())
						bytes = sha512.ComputeHash(fs);
					break;

				case "md5":
				default:
					using (MD5CryptoServiceProvider md5 =
						new MD5CryptoServiceProvider())
						bytes = md5.ComputeHash(fs);
					break;
			}

			// Cleanup
			fs.Close();
			fs = null;

			return BitConverter
					.ToString(bytes)
					.Replace("-", "")
					.ToLower();
		}

		/// <summary>
		/// Returns the page slug or converts a page title into a slug
		/// </summary>
		public static string GetSlug(string val, string d, int length = 45, bool lower = false)
		{
			val = Util.DefaultFlatString(val, d, length);

			// Duplicate spaces
			val = Regex.Replace(val, @"[\s-]+", " ").Trim();
			val = Util.NormalizeString(val); // Remove special chars
			val = Regex.Replace(val, @"\s", "-"); // Spaces to dashes


			// If we still couldn't get a proper string, generate one from default
			val = (String.IsNullOrEmpty(val) || val.Length < 3) ? d :
			val.Substring(0, val.Length <= length ? val.Length : length).Trim();

			
			return (lower) ? val.ToLower() : val;
		}

		private static string NormalizeString(string txt)
		{
			if (!String.IsNullOrEmpty(txt))
				txt = txt.Normalize(NormalizationForm.FormD);

			StringBuilder sb = new StringBuilder();

			sb.Append(
				txt.Normalize(NormalizationForm.FormD).Where(
					c => CharUnicodeInfo.GetUnicodeCategory(c)
					!= UnicodeCategory.NonSpacingMark).ToArray()
				);

			return sb.ToString().Normalize(NormalizationForm.FormD);
		}

		/// <summary>
		/// Gets an array of cleaned tags
		/// </summary>
		/// <param name="txt">A comma delimited string of tags</param>
		/// <returns>Array of cleaned tags</returns>
		public static string[] GetTags(string txt, bool lower = false)
		{
			string[] tags = txt.Split(',');
			ArrayList clean = new ArrayList();

			for (int i = 0; i < tags.Length; i++)
			{
				tags[i] = DefaultFlatString(tags[i], " ").Trim();

				if (!string.IsNullOrEmpty(tags[i]))
					tags[i] = NormalizeString((lower)? 
						tags[i].ToLower() : tags[i]);

				// Don't want to repeat
				if (!clean.Contains(tags[i])) 
						clean.Add(tags[i]);
			}

			return (string[])clean.ToArray(typeof(string));
		}

		/// <summary>
		/// Gets an array of cleaned keywords
		/// </summary>
		/// <param name="txt">A comma delimited string of keywords</param>
		/// <param name="limit">Limit s the number of tags returned</param>
		/// <param name="tolower">Optional parameter to convert the text to lowercase</param>
		/// <returns>Array of cleaned keywords</returns>
		public static List<string> GetKeywords(string txt, int limit, bool tolower = true)
		{
			string[] tags = txt.Split(',');
			List<string> clean = new List<string>();

			for (int i = 0; i < tags.Length; i++)
			{
				tags[i] = Util.DefaultFlatString(tags[i], "");

				if (!String.IsNullOrEmpty(tags[i]))
				{
					if (tolower)
						clean.Add(tags[i].ToLower());
					else
						clean.Add(tags[i]);
				}
			}

			return clean;
		}

		/// <summary>
		/// Shorten a give text block followed by an ellipse
		/// </summary>
		public static string TrimText(string strInput, int intNum)
		{
			strInput = strInput.Replace("\r", string.Empty)
				.Replace("\n", string.Empty);
			if ((strInput.Length > intNum) && (intNum > 0))
			{
				strInput = strInput.Substring(0, intNum) + "...";
			}
			return strInput;
		}

		/// <summary>
		/// Checks whether string has value or sets default it doesn't or is at 0
		/// </summary>
		public static int DefaultInt(string val, int d, int? min)
		{
			int tmp = 0;

			if (!Int32.TryParse(val, out tmp))
				tmp = d;

			if (min.HasValue)
				if (tmp <= min.Value) tmp = d;

			return tmp;
		}

		/// <summary>
		/// Checks whether nullable int has value or sets default it doesn't or is at 0
		/// </summary>
		public static int DefaultInt(int? val, int d, int? min)
		{
			val = val ?? d;
			if (min.HasValue)
				if (val.Value <= min.Value) val = d;

			return val.Value;
		}

		/// <summary>
		/// Checks whether nullable bool has value or sets default it doesn't
		/// </summary>
		public static bool DefaultBool(bool? val, bool d)
		{
			val = val ?? d;
			return val.Value;
		}

		/// <summary>
		/// Checks whether nullable bool has value or sets default it doesn't
		/// </summary>
		public static bool DefaultBool(string val, bool d)
		{
			bool tmp = d;

			if (Boolean.TryParse(val, out tmp))
				return tmp;

			return d;
		}

		/// <summary>
		/// Returns a flat (no line breaks) string or a default value if empty
		/// </summary>
		public static string DefaultFlatString(string val, string d, int l = 255)
		{
			return Util.DefaultString(val, d, l).Replace(Environment.NewLine, "");
		}

		/// <summary>
		/// Checks whether nullable string has value or sets default it doesn't or is at empty
		/// </summary>
		public static string DefaultString(string val, string d, int l = 255)
		{
			if (string.IsNullOrEmpty(val)) val = d;

			val.Replace("\r", Environment.NewLine)
				.Replace("\n", Environment.NewLine);

			if ((val.Length > 0) && (val.Length > l))
				val = val.Substring(0, l-1);

			return val;
		}

		/// <summary>
		/// Converts a string value to a DateTime object or returns the default value on failure
		/// </summary>
		public static DateTime DefaultDate(string val, DateTime d)
		{
			DateTime dt;
			if (DateTime.TryParse(val, out dt))
				return dt;

			return d;
		}

		/// <summary>
		/// Converts a nullable date value to a DateTime object or returns the default value on failure
		/// </summary>
		public static DateTime DefaultDate(DateTime? val, DateTime d)
		{
			DateTime dt;
			dt = (val.HasValue) ? val.Value : d;

			return d;
		}

		/// <summary>
		/// Gets the current user's IP address
		/// </summary>
		public static string GetUserIP()
		{
			// Connecting through a proxy?
			string ip = HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];

			// None found
			if (string.IsNullOrEmpty(ip) || ip.ToLower() == "unknown")
				ip = HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"];

			return ip;
		}

		public static string GetEmail(string v)
		{
			string email = @"^[\w!#$%&'*+\-/=?\^_`{|}~]+(\.[\w!#$%&'*+\-/=?\^_`{|}~]+)*" +
							@"@((([\-\w]+\.)+[a-zA-Z]{2,4})|(([0-9]{1,3}\.){3}[0-9]{1,3}))$";

			if (!string.IsNullOrEmpty(v))
				if (Regex.IsMatch(v, email))
					return v;

			// Didn't match the email format, so sent a cleaned string
			return Util.DefaultFlatString(v, "");
		}
	}
}

PostRepository. This one’s a bit long…

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Road.Helpers;

namespace Road.Models
{
	public class PostRepository
	{
		// Common DataContext
		private readonly CMDataContext db;

		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="context">Global context</param>
		public PostRepository(CMDataContext context)
		{
			this.db = context;
		}

		#region Topic display methods


		/// <summary>
		/// Gets a topic by the given id and finds corresponding replies if any
		/// </summary>
		/// <param name="id">Topic id to search</param>
		/// <param name="index">Current page index</param>
		/// <param name="limit">Page size limit</param>
		/// <param name="unapproved">Include unapproved replies (optional)</param>
		/// <param name="status">Array of reply status types (optional)</param>
		/// <param name="newestFirst">Sort by newest replies first (optional)</param>
		public Topic TopicById(int id, int index, int limit,
			bool unapproved = false, ReplyStatus[] status = null, bool newestFirst = false)
		{
			var query = from p in db.Posts
						where p.PostId == id
						select p;

			Topic topic = TopicQuery(query).FirstOrDefault();

			// We have a topic and replies were also requested
			if (topic != null && limit > 0)
			{
				var rquery = from p in db.Posts
							 join pa in db.PostRelations on p.PostId equals pa.ParentId
							 where pa.ParentId == topic.Id
							 select p;

				topic.Replies =
					ReplyQuery(rquery, unapproved, status, newestFirst).ToPagedList(index, limit);
			}

			return topic;
		}

		/// <summary>
		/// Gets a list of topics (most basic request, usually for frontpage)
		/// </summary>
		/// <param name="limit">Page size limit</param>
		/// <param name="unapproved">Include unapproved topics (optional)</param>
		/// <param name="status">Array of status topic status types (optional)</param>
		/// <param name="newestFirst">Sort by newest topics first (optional)</param>
		/// <returns>List of topics</returns>
		public List<Topic> TopicList(int limit, bool unapproved = false,
			TopicStatus[] status = null, bool newestFirst = true)
		{
			var query = from p in db.Posts
						select p;

			return TopicQuery(query, unapproved, status, newestFirst).Take(limit).ToList();
		}

		/// <summary>
		/// Gets a paged list of topics
		/// </summary>
		/// <param name="index">Current page index</param>
		/// <param name="limit">Page size limit</param>
		/// <param name="unapproved">Include unapproved topics (optional)</param>
		/// <param name="status">Array of status topic status types (optional)</param>
		/// <param name="newestFirst">Sort by newest topics first (optional)</param>
		/// <returns>Paged list of topics</returns>
		public PagedList<Topic> TopicPageList(int index, int limit,
			bool unapproved = false, TopicStatus[] status = null, 
			bool newestFirst = true)
		{
			var query = from p in db.Posts
						select p;

			return TopicQuery(query, unapproved, status, newestFirst).ToPagedList(index, limit);
		}

		/// <summary>
		/// Gets a paged list of topics belonging to a tag(s)
		/// (This uses the lowercase TagName property)
		/// </summary>
		/// <param name="tag">Array of tags to search</param>
		/// <param name="index">Current page index</param>
		/// <param name="limit">Page size limit</param>
		/// <param name="unapproved">Include unapproved topics (optional)</param>
		/// <param name="status">Array of status topic status types (optional)</param>
		/// <param name="newestFirst">Sort by newest topics first (optional)</param>
		/// <returns>Paged list of topics</returns>
		public PagedList<Topic> TopicsByTag(string[] tag, int index, int limit,
			bool unapproved = false, TopicStatus[] status = null, bool newestFirst = true)
		{
			var query = from t in db.PostTags
						join pt in db.PostTagRelations on t.TagId equals pt.TagId
						join p in db.Posts on pt.PostId equals p.PostId
						where tag.Contains(t.TagName)
						select p;

			return TopicQuery(query, unapproved, status, newestFirst).ToPagedList(index, limit);
		}

		/// <summary>
		/// Gets an individual topic belonging to a list of tag(s)
		/// </summary>
		/// <param name="tagId">Array of tag Ids to search</param>
		/// <param name="index">Current page index</param>
		/// <param name="limit">Page size limit</param>
		/// <param name="unapproved">Include unapproved topics (optional)</param>
		/// <param name="status">Array of status topic status types (optional)</param>
		/// <param name="newestFirst">Sort by newest topics first (optional)</param>
		/// <returns>Paged list of topics</returns>
		public PagedList<Topic> TopicsByTagId(int[] tagId, int index, int limit,
			bool unapproved = false, TopicStatus[] status = null, bool newestFirst = true)
		{
			var query = from t in db.PostTags
						join pt in db.PostTagRelations on t.TagId equals pt.TagId
						join p in db.Posts on pt.PostId equals p.PostId
						where tagId.Contains(t.TagId)
						select p;

			return TopicQuery(query, unapproved, status, newestFirst).ToPagedList(index, limit);
		}

		/// <summary>
		/// Gets a paged list of topics by the search criteria
		/// </summary>
		/// <param name="search">Title and body search terms</param>
		/// <param name="index">Current page index</param>
		/// <param name="limit">Page size limit</param>
		/// <param name="unapproved">Include unapproved topics (optional)</param>
		/// <param name="status">Array of status topic status types (optional)</param>
		/// <param name="newestFirst">Sort by newest topics first (optional)</param>
		/// <returns>Paged list of topics</returns>
		public PagedList<Topic> TopicsBySearch(string search, int index, int limit,
			bool unapproved = false, TopicStatus[] status = null, bool newestFirst = true)
		{
			var query = from p in db.Posts
						where p.BodyText.Contains(search) || p.Title.Contains(search)
						select p;

			return TopicQuery(query, unapproved, status, newestFirst).ToPagedList(index, limit);
		}

		/// <summary>
		/// Gets a paged list of replies by the search criteria 
		/// (only searches the bodytext)
		/// </summary>
		/// <param name="search">Search terms</param>
		/// <param name="index">Current page index</param>
		/// <param name="limit">Page size limit</param>
		/// <param name="unapproved">Include unapproved topics (optional)</param>
		/// <param name="status">Array of topic status types (optional)</param>
		/// <param name="newestFirst">Sort by newest topics first (optional)</param>
		/// <returns>Paged list of topics</returns>
		public PagedList<Reply> RepliesBySearch(string search, int index, int limit,
			bool unapproved = false, ReplyStatus[] status = null, bool newestFirst = true)
		{
			var query = from p in db.Posts
						where p.BodyText.Contains(search)
						select p;

			return ReplyQuery(query, unapproved, status, newestFirst).ToPagedList(index, limit);
		}

		#endregion

		#region Save methods

		/// <summary>
		/// Saves or creates a new reply under the given topic
		/// </summary>
		/// <param name="topic">Topic the reply belongs to</param>
		/// <param name="reply">Reply to save</param>
		public Reply SaveReply(Topic topic, Reply reply)
		{
			Post p = null;
			DateTime dt = DateTime.UtcNow;

			if (reply.Id != 0)
			{
				p = (from post in db.Posts
					where post.PostId == reply.Id
					select post).FirstOrDefault();
			}
			else
			{
				p = new Post();
				p.CreatedDate = dt;
				p.ReplyCount = 0;
				p.ViewCount = 0;
				db.Posts.InsertOnSubmit(p);
			}

			p.Approved = reply.Approved;
			p.Status = (byte)reply.Status;
			p.Threshold = reply.Threshold;
			p.LastModified = dt;
			p.BodyHtml = reply.Body;
			p.BodyText = reply.Summary;

			// Save reply
			db.SubmitChanges();

			// If this is a new reply...
			if (p.PostId > 0 && reply.Id == 0)
			{
				// We now have an Id to set
				reply.Id = p.PostId;

				// Create Author, PostRelation and PostAuthor relationships
				Author a = new Author();
				a.MemberId = reply.CreatedBy.Id;
				a.AuthorIP = reply.CreatedBy.IP;
				a.AuthorName = reply.CreatedBy.Name;
				a.AuthorEmail = reply.CreatedBy.Email;
				a.AuthorWeb = reply.CreatedBy.Web;
				db.Authors.InsertOnSubmit(a);


				PostRelation pr = new PostRelation();
				pr.ParentId = topic.Id;
				pr.PostId = p.PostId;
				db.PostRelations.InsertOnSubmit(pr);
				db.SubmitChanges();

				if (a.AuthorId > 0)
				{
					PostAuthor pa = new PostAuthor();
					pa.AuthorId = reply.CreatedBy.Id;
					pa.PostId = reply.Id;
					db.PostAuthors.InsertOnSubmit(pa);
					db.SubmitChanges();
				}
			}

			return reply;
		}

		/// <summary>
		/// Saves or creates a new topic
		/// </summary>
		/// <param name="topic">Topic to save</param>
		/// <returns>Returns the saved topic</returns>
		public Topic SaveTopic(Topic topic)
		{
			Post p = null;
			DateTime dt = DateTime.UtcNow;

			if (topic.Id != 0)
			{
				p = (from post in db.Posts
					 where post.PostId == topic.Id
					 select post).FirstOrDefault();
			}
			else
			{
				p = new Post();
				p.CreatedDate = dt;
				db.Posts.InsertOnSubmit(p);
			}

			p.Title = topic.Name;
			p.Approved = topic.Approved;
			p.Status = (byte)topic.Status;
			p.Threshold = topic.Threshold;

			p.LastModified = dt;
			p.BodyHtml = topic.Body;
			p.BodyText = topic.Summary;

			p.ViewCount = topic.ViewCount;
			p.ReplyCount = topic.ReplyCount;

			// Save
			db.SubmitChanges();

			// If this is a new topic...
			if (p.PostId > 0 && topic.Id == 0)
			{
				// Set the Id, now that we have one
				topic.Id = p.PostId;

				// Create author and set relationship
				Author a = new Author();
				a.MemberId = topic.CreatedBy.MemberId;
				a.AuthorIP = topic.CreatedBy.IP;
				a.AuthorName = topic.CreatedBy.Name;
				a.AuthorEmail = topic.CreatedBy.Email;
				a.AuthorWeb = topic.CreatedBy.Web;
				db.Authors.InsertOnSubmit(a);

				PostRelation pr = new PostRelation();
				pr.ParentId = p.PostId; // Same since it's a topic
				pr.PostId = p.PostId;
				db.PostRelations.InsertOnSubmit(pr);

				db.SubmitChanges();

				if (a.AuthorId > 0)
				{
					PostAuthor pa = new PostAuthor();
					pa.AuthorId = a.AuthorId;
					pa.PostId = p.PostId;
					db.PostAuthors.InsertOnSubmit(pa);

					db.SubmitChanges();
				}
			}


			topic.Slug = Util.GetSlug(p.Title, "topic");


			ApplyTags(topic.Tags.ToList(), topic);

			return topic;
		}

		#endregion

		#region Tag methods

		/// <summary>
		/// Gets a list of tags by a search string 
		/// (usually for tag autocomplete)
		/// </summary>
		/// <param name="tag">Tag search string</param>
		/// <param name="limit">Page size limit</param>
		/// <returns></returns>
		public List<Tag> TagsByName(string tag, int limit)
		{
			var query = from t in db.PostTags
						orderby t.TagName ascending
						where t.TagName.StartsWith(tag)
						select new Tag
						{
							Id = t.TagId,
							Name = t.TagName,
							Slug = t.Slug,
							DisplayName = t.TagName
						};

			if (limit > 0)
				query = query.Take(limit);

			return query.ToList();
		}

		/// <summary>
		/// Associates a list of tags with the given topic
		/// </summary>
		/// <param name="tags">Tags to link to topic</param>
		/// <param name="topic">Target topic</param>
		private void ApplyTags(List<Tag> tags, Topic topic)
		{
			List<PostTagRelation> existing = (from pt in db.PostTagRelations
											  join t in db.PostTags on pt.TagId equals t.TagId
											  where pt.PostId == topic.Id
											  select pt).ToList();

			// Clean existing relationships
			db.PostTagRelations.DeleteAllOnSubmit(existing);
			db.SubmitChanges();

			// Setup the new relationships
			List<PostTagRelation> newrelation = new List<PostTagRelation>();

			// Store the new tags and get the complete list of tags
			tags = StoreTags(tags, topic.CreatedBy);

			foreach (Tag t in tags)
			{
				PostTagRelation tag = new PostTagRelation();
				tag.TagId = t.Id;
				tag.PostId = topic.Id;
				newrelation.Add(tag);
			}
			// Save the new tag relationships
			db.PostTagRelations.InsertAllOnSubmit(newrelation);
			db.SubmitChanges();
		}


		/// <summary>
		/// Finds existing tags and creates new tags with the associated creator if
		/// the tag doesn't exist
		/// </summary>
		/// <param name="tags">List of tags to create/find</param>
		/// <param name="creator">Tag creator</param>
		/// <returns>List of found and newly created tags</returns>
		private List<Tag> StoreTags(List<Tag> tags, Creator creator)
		{
			// Complete list of all tags
			List<Tag> complete = new List<Tag>();

			// Created date
			DateTime dt = DateTime.UtcNow;

			string[] search = tags.Select(tg => tg.Name).ToArray();
			string[] existing = (from t in db.PostTags
								where search.Contains(t.TagName)
								select t.TagName).ToArray();

			// Tags except those already in the database
			string[] newtags = search.Except(existing).ToArray();

			// We have new tags to save
			if (newtags.Length > 0)
			{
				List<PostTag> savetags = (from tg in tags
								   where newtags.Contains(tg.Name)
								   select new PostTag
								   {
									   DisplayName = tg.DisplayName,
									   TagName = tg.DisplayName.ToLower(),
									   Slug = Util.GetSlug(tg.DisplayName, tg.Name),
									   Status = (byte)TagStatus.Open,
									   LastModified = dt,
									   CreatedDate = dt,
									   BodyHtml = "",
									   BodyText = ""
								   }).ToList();

				if (savetags.Count() > 0)
				{
					db.PostTags.InsertAllOnSubmit(savetags);
					db.SubmitChanges();

					// Create author info for each new tag
					Author author = getAuthor(creator);
					List<TagAuthor> authors = (from tg in savetags
											   select new TagAuthor
											   {
												   AuthorId = author.AuthorId,
												   TagId = tg.TagId
											   }).ToList();
					db.TagAuthors.InsertAllOnSubmit(authors);
					db.SubmitChanges();
				}

				// Get all existing and newly inserted tags
				complete = (from tg in db.PostTags
							where search.Contains(tg.TagName)
							select new Tag
							{
								Id = tg.TagId,
								Name = tg.TagName,
								DisplayName = tg.DisplayName,
								Slug = tg.Slug
							}).ToList();
			}

			return complete;
		}

		#endregion


		#region Queries

		/// <summary>
		/// Creates a deferred execution IQueryable to search topics
		/// </summary>
		/// <param name="posts">Initial search query</param>
		/// <returns>Topic IQueryable</returns>
		private IQueryable<Topic> TopicQuery(IQueryable<Post> posts, 
			bool unapproved = false, TopicStatus[] status = null,
			bool newestFirst = true)
		{
			var query = from p in posts
						join au in db.PostAuthors on p.PostId equals au.PostId
						join a in db.Authors on au.AuthorId equals a.AuthorId
						join m in db.Members on au.AuthorId equals m.MemberId into author
						from auth in author.DefaultIfEmpty() // Empty if anonymous post

						let postauthor = getCreator(a, auth)
						let tags = GetTagsForTopic(p.PostId)

						select new { p, postauthor, tags };

			// Include unapproved topics?
			query = (unapproved) ?
				query.Where(r => r.p.Approved == false) :
				query.Where(r => r.p.Approved == true);

			// Any status other than "Open"?
			query = (status != null) ?
				query = query.Where(r => status.Contains((TopicStatus)r.p.Status)) :
				query = query.Where(r => r.p.Status == (byte)TopicStatus.Open);

			// Sort by new topics first?
			query = (newestFirst) ?
				query.OrderByDescending(r => r.p.CreatedDate) :
				query.OrderBy(r => r.p.CreatedDate);

			return from r in query
				   select new Topic
				   {
					   Id = r.p.PostId,
					   Name = r.p.Title,
					   CreatedBy = r.postauthor,
					   CreatedDate = r.p.CreatedDate,
					   LastModified = r.p.LastModified,
					   Summary = r.p.BodyText,
					   Slug = Util.GetSlug(r.p.Title, "topic", 50, true),
					   Tags = new LazyList<Tag>(r.tags),
					   ViewCount = r.p.ViewCount,
					   ReplyCount = r.p.ReplyCount,
					   Threshold = (float)r.p.Threshold,
					   Status = (TopicStatus)r.p.Status
				   };
		}

		/// <summary>
		/// Creates a deferred execution IQueryable to search replies
		/// </summary>
		/// <param name="posts">Initial posts query</param>
		/// <param name="status">Status restriction array</param>
		/// <param name="newestFirst">Sort by new replies first</param>
		/// <returns>Reply IQueryable</returns>
		private IQueryable<Reply> ReplyQuery(IQueryable<Post> posts,  bool unapproved, ReplyStatus[] status, bool newestFirst)
		{
			var query = from p in posts
						join au in db.PostAuthors on p.PostId equals au.PostId
						join a in db.Authors on au.AuthorId equals a.AuthorId
						join m in db.Members on au.AuthorId equals m.MemberId into author
						from auth in author.DefaultIfEmpty() // Empty if anonymous post

						let postauthor = getCreator(a, auth)
						select new { p, postauthor };

			// Include unapproved replies?
			query = (unapproved) ?
				query.Where(r => r.p.Approved == false) :
				query.Where(r => r.p.Approved == true);

			// Any status other than "Open"?
			query = (status != null) ?
				query = query.Where(r => status.Contains((ReplyStatus)r.p.Status)) :
				query = query.Where(r => r.p.Status == (byte)ReplyStatus.Open);

			// Sort by new replies first?
			query = (newestFirst) ?
				query.OrderByDescending(r => r.p.CreatedDate) :
				query.OrderBy(r => r.p.CreatedDate);

			return from r in query.AsQueryable()
				   select new Reply
				   {
					   Id = r.p.PostId,
					   CreatedBy = r.postauthor,
					   CreatedDate = r.p.CreatedDate,
					   LastModified = r.p.LastModified,
					   Body = r.p.BodyHtml,
					   Threshold = (float)r.p.Threshold
				   };
		}

		/// <summary>
		/// Helper finds the tags for a topic by id
		/// </summary>
		/// <param name="id">Topic id to search</param>
		/// <returns>IQueryable Tag</returns>
		private IQueryable<Tag> GetTagsForTopic(int id)
		{
			return from t in db.PostTags
				   join pt in db.PostTagRelations on t.TagId equals pt.TagId
				   where pt.PostId == id
				   select new Tag
				   {
					   Id = t.TagId,
					   Name = t.TagName,
					   Slug = t.Slug,
					   DisplayName = t.DisplayName,
					   CreatedDate = t.CreatedDate,
					   LastModified = t.LastModified
				   };
		}

		#endregion

		#region Author/Creator Helpers

		/// <summary>
		/// Helper function generates a save friendly Author from a given Creator
		/// </summary>
		/// <param name="c">Creator data</param>
		/// <returns>Author object</returns>
		private static Author getAuthor(Creator c)
		{
			if (c == null)
				return null;

			Author author = new Author();
			if (c.Id > 0)
			{
				author.AuthorId = c.Id;
			}
			else
			{
				author.AuthorEmail = c.Email;
				author.AuthorWeb = c.Web;
			}

			author.AuthorIP = c.IP;
			author.AuthorName = c.Name;

			return author;
		}

		/// <summary>
		/// Finds or creates a Creator object from given author information
		/// </summary>
		/// <param name="a">Saved author information</param>
		/// <param name="m">Optional membership information</param>
		/// <returns>Composite Creator object</returns>
		private static Creator getCreator(Author a, Member m)
		{
			Creator au = new Creator();

			au.IP = a.AuthorIP;
			if (m != null)
			{
				au = getCreator(m);
			}
			else
			{
				au.LastModified = DateTime.MinValue;
				au.Id = a.AuthorId;
				au.Name = a.AuthorName;
				au.DisplayName = a.AuthorName;
				au.Email = a.AuthorEmail;
				au.Web = a.AuthorWeb;
			}
			return au;
		}

		/// <summary>
		/// Helper function generates a Creator object from membership info
		/// </summary>
		/// <param name="m">Member object</param>
		/// <returns>Composite Creator object</returns>
		private static Creator getCreator(Member m)
		{
			return new Creator
			{
				Id = m.MemberId,
				Name = m.Username,
				DisplayName = m.DisplayName,
				Email = m.Email,
				Web = m.Web,
				Slug = Util.GetSlug(m.Username, m.MemberId.ToString(), 70),
				CreatedDate = m.CreatedDate,
				LastModified = m.LastActivity,
				Avatar = m.Avatar
			};
		}

		#endregion
	}
}

Permanently turning off Google Instant (update)

February 2, 2012

If anyone noticed, the Google instant configuration has now changed since the last time. Looks like it was very recent.

Before it was simply an on off switch, the setting of which sometimes Google remembered, but most of the time didn’t. Now you have more options.

Clicking on the gear on the top righthand bar will bring up this page…

Never, you bastards!

I don’t know if this time it would make any difference at all, but the last time, the setting only lasted as long as you didn’t use any other Google service (E.G. Gmail) and then logout.

It’s 3:09AM right now, so I’m not going to bother testing this. Let’s just hope that this time they took a hint from all the feedback.

Basic: A simple theme for corporate-ish sites

February 2, 2012

When I say corporate-ish, I mean totally serious and devoid of any shenanigans that may offend your boss… or you, if you’re that type of person. Deadline: 1 hour.

I was going to make this post before, but I just didn’t have the time. This was another design partly inspired by need and part by reader request. I got an email this evening by somone with a very similar need so I figured I’d post it here for anyone else to pickup.

“Basic” would fall into the simple-but-not-simplistic category of designs in that it’s got the bare essentials only, similar to the Simply theme, but with even fewer bells and whistles and yet is still clear, functional and usable. While it can be used as-is, it’s really a learning tool ment to be taken apart and reassembled with any additional hacks by the user. I also left out a lot of older cruft from pre HTML 5 designs as much as possible and the CSS also reflects more v.3 usage.

Basic: Front page

 

While creating this theme, I realised how similar the header is to the asp.net tutorial pages, especially the article header below the top links. This was purely coincidence since this was meant to be a bare minimum theme and Microsoft has a reputation for being terribly unexciting.

Header uses the same font and sizes

 

But since it looked similar at this point anyway, I decided to add a matching breadcrumb navigation…

Since we're this close. Why not go all the way?

 

The difference of course is that the HTML and CSS in my version are completely different. I.E. Simpler and straightforward. In the MS example, they’re using a paragraph with actual backlashes and s to denote seperators. This is way overkill and pointless so in my example, I’m using the <nav> tag and a <ul> list.

The backslashes are added via CSS “after” and “content” :

nav.crumbs ul li:after
{
	content: "/";
}
nav.crumbs ul li:last-child:after
{
	content: "";
}

Since most modern browsers support this anyway, and the fact that these are not inline links, but list items, it pretty well does exactly what a navigation list is meant to do.

The breadcrumbs are shown on the corresponding “topics” example page.

Basic with breadcrumbs

Borrowing from the Discussion Forum mockup where I had placed the new topic, login and registration forms on the same page, I included them on the bottom here and used an expandable “component” function. Basically takes any container with a “component” class and turns the elements inside into widgets with the first <h2> element inside as the handler instead of using jQuery UI tabs or accordion.

The JavaScript is otherwise the same as the forum mockup. Here’s that “component” function :

function initComps() {
	$('.components').children().each(function(i) {
		var b = $(this);

		// First element is usually the title/handler
		var hd = b.children('h2');

		// All other elements need to be wrapped up
b.children(':not(:first-child)').wrapAll('</pre>
<div class="blockct"></div>
<pre>');
		var ct = b.children('.blockct');
		var lnk = hd.append(' <a href="#" rel="showhide">(show)</a>');

		// Find the toggle link in the header and assign the control
		hd.children('a[rel="showhide"]').click(function (e) {
			ct.slideToggle();	// Toggle contents

			// Change link text
			$(this).text(($(this).text() == "(show)")? "(hide)" : "(show)");
			return false;	// Don't return
		});

		ct.hide(); // Initially hide everything
	});
}

If you think the opener to this post is odd : Not that I’m calling my boss boring…

…but I can’t think of a way to finish that sentence.

- Bart Simpson

RE: Lost in Transmission

February 1, 2012

This was going to be just a comment on a recent blog post by stewardsofearth, but since it was getting to be too long, I just decided to turn it into a post instead. Also, I wanted to link the post here as that blog is good reading for anyone interested in sustainable living.

There’s a high initial cost to sustainable alternatives that some people are unwilling or unable to invest in. E.G. Solar panels, while getting cheaper are still not cheap enough for a lot of people. Same with LED lights vs CFL and plain ‘ol incandescent. It takes foresight and a willingness to take the plunge and, of course, it would help if they don’t just take all the myths about solar at face value.

But, as you say, it does take a combination of sustainable alternatives and a change in lifestyle to make it all work. We’ve just been spoiled for the past few decades by the abundance of… well… everything. Credit, oil, jobs, homes.

If there is any upside to this bad economy, it’s that children who grow up this decade will learn the value of frugality, efficiency and the pitfalls of conspicuous consumption. Keeping up with the Jonses doesn’t make much sense when the Jonses are about to lose their McMansion to foreclosure. Likewise, it doesn’t make sense to waste resources like they’re going out of style… which we’re going out of, just not in style.

There’s another bump in the road to the widespread adoption of sustainability…

The Cult of Me

There’s a particularly insidious and rather socially self-defeating mindset among people who shun alternative energy and a frugal lifestyle. It’s the I have to “sacrifice” this and that, but at the same time the effective end result is insignificant therefore the “sacrifices” are ultimately pointless.

Let’s say I’m in the habit of buying golf tees, whether I go golfing or not, just so I’ll always have a handy supply of the brand I select. Known for using pure cedar rather than biodegradable wood composite, the brand is not as sustainable. When I’m advised by my friend who’s well versed sustainability that there’s virtually no difference with regard to performance and that I’d be helping the environment by switching to composite, I laugh and say that “it’s just a golf tee. What’s the big deal?”

To me in my own I-choose-what-I-choose because that’s what I’ve always chosen mentality and the ingrained idea that this is such a seemingly insignificant thing, it’s a perfectly reasonable response. And perfectly wrong.

What I choose is important to me and simultaneously insignificant. We call that double-think.

The problem isn’t how small the golf tee is to me or how insignificant a choice it is in the grand scheme of things, it’s that there are countless others who think the exact same way. When those countless others do the same thing I did, laugh at the apparently simple change, the company that makes the tee keeps cutting down more cedar.

We’re happy to see things from our own perspective and we always do whenever it’s convenient. But from our perspective – our own narrow perspective – we miss quite a bit of just how large our sphere of influence can be. We also fail to grasp that spheres of influence are cumulative and even an apparently insignificant change, if adopted by many, will have a much larger effect simply because our interdependence.

Site of the Week: Unibrow Club

January 31, 2012

I’m all for social acceptance, especially when the detraction is over something stupid. Imagine my glee when I came across the Unibrow Club.

http://www.unibrowclub.com/

From their Mission page :

At Unibrow Club we try to answer all those ancient questions such as what is unibrow, what is the difference between unibrow and monobrow, does tripplebrow exists and so on. Get all your unibrow questions answered here.

I didn’t even know there was such a thing as a “tripplebrow”. Suddenly I don’t feel so bad about having South Asian “hair” genes.